beast-agent 2.12.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.12.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);
@@ -3529,6 +3597,16 @@ class Engine {
3529
3597
  else if (/\b(npm (run )?(dev|start)|npx serve|live-server)\b/i.test(cmdStr)) port = 3000;
3530
3598
  if (port) bcArtifacts.serverUrl = 'http://localhost:' + port;
3531
3599
  }
3600
+ /* Expo dev server: exp://<LAN-IP>:<port> çıktıdan yakalanır —
3601
+ telefon/Expo Go için QR renderer'a canlı iletilir */
3602
+ const xm = /exp:\/\/[^\s'"<>]+/i.exec(hay);
3603
+ if (xm) {
3604
+ const eurl = xm[0].replace(/[)\].,;:'"]+$/, '');
3605
+ if (bcArtifacts.expUrl !== eurl) {
3606
+ bcArtifacts.expUrl = eurl;
3607
+ emit({ type: 'bc-expurl', url: eurl });
3608
+ }
3609
+ }
3532
3610
  }
3533
3611
  } catch {}
3534
3612
  }
@@ -4068,6 +4146,7 @@ const skills = require('./skills');
4068
4146
  );
4069
4147
  const assistant = { role: 'assistant', content: res.content || '' };
4070
4148
  if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
4149
+ if (res.reasoning) assistant.reasoning_content = res.reasoning; /* thinking geri-besleme */
4071
4150
  msgs.push(assistant);
4072
4151
  if (!res.toolCalls || !res.toolCalls.length) return res.content || '(alt-agent boş döndü)';
4073
4152
  for (const tc of res.toolCalls) {
@@ -0,0 +1,279 @@
1
+ 'use strict';
2
+
3
+ /* ---------- BEAST × HEADROOM (OPSİYONEL token sıkıştırma) ----------
4
+ Ayarlardan (Kurulum sekmesi) açılıp kapanır; kapalıyken hiçbir şey yapmaz.
5
+ Açıkken: LLM istekleri yerel headroom proxy'sinden geçer — araç çıktıları,
6
+ loglar ve JSON yükleri modele gitmeden sıkıştırılır (testte %39-71 tasarruf).
7
+ Kurulum: uv tool install "headroom-ai[proxy]" (uv yoksa hata bildirir).
8
+ Her provider için AYRI proxy portu (base URL farkları için); llm.js
9
+ providerId → port haritasıyla yönlendirir, kapatınca düz bağlanır. */
10
+
11
+ const { spawn } = require('child_process');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const { emitInstallProgress } = require('./progressbus');
16
+
17
+ const PORT_BASE = 18787;
18
+ const MAX_PROXIES = 8;
19
+
20
+ const state = {
21
+ enabled: false,
22
+ installing: false,
23
+ installFailed: false,
24
+ installError: '',
25
+ starting: false,
26
+ proxies: new Map(), // providerId -> { proc, port, baseUrl, ready }
27
+ providerGetter: null, // () => [{ id, name, baseUrl }]
28
+ statusSink: null, // (probe) => void — main renderer'a akıtır
29
+ };
30
+
31
+ function _emit() {
32
+ try { if (state.statusSink) state.statusSink(probe()); } catch {}
33
+ }
34
+
35
+ function headroomExe() {
36
+ const local = path.join(os.homedir(), '.local', 'bin', 'headroom.exe');
37
+ if (fs.existsSync(local)) return local;
38
+ try {
39
+ require('child_process').execSync('where headroom', { timeout: 5000, stdio: 'pipe' });
40
+ return 'headroom';
41
+ } catch {}
42
+ return '';
43
+ }
44
+
45
+ function uvExe() {
46
+ const local = path.join(os.homedir(), '.local', 'bin', 'uv.exe');
47
+ if (fs.existsSync(local)) return local;
48
+ try {
49
+ require('child_process').execSync('where uv', { timeout: 5000, stdio: 'pipe' });
50
+ return 'uv';
51
+ } catch {}
52
+ return '';
53
+ }
54
+
55
+ function probe() {
56
+ return {
57
+ installed: !!headroomExe(),
58
+ installing: state.installing,
59
+ failed: state.installFailed,
60
+ error: state.installError,
61
+ enabled: state.enabled,
62
+ starting: state.starting,
63
+ proxies: [...state.proxies.values()].map((p) => ({ port: p.port, baseUrl: p.baseUrl, ready: p.ready })),
64
+ };
65
+ }
66
+
67
+ /* ---------- KURULUM (arka plan) ----------
68
+ uv tool install headroom-ai[proxy] — kendi izole Python 3.13 ortamını kurar;
69
+ sistemde Python gerekmez. uv yoksa pip fallback, o da yoksa elle yönlendirir. */
70
+ function autoInstall() {
71
+ if (state.installing || headroomExe()) return;
72
+ const uv = uvExe();
73
+ if (!uv) {
74
+ state.installFailed = true;
75
+ state.installError = 'uv bulunamadı — kur: winget install astral-sh.uv';
76
+ _emit();
77
+ return;
78
+ }
79
+ state.installing = true;
80
+ state.installFailed = false;
81
+ state.installError = '';
82
+ _emit();
83
+ emitInstallProgress('headroom', { pct: 4 });
84
+ const proc = spawn(uv, ['tool', 'install', '--python', '3.13', 'headroom-ai[proxy]'], { windowsHide: true });
85
+ let buf = '';
86
+ let stage = 4;
87
+ const bump = () => {
88
+ stage = Math.min(92, stage + 7);
89
+ emitInstallProgress('headroom', { pct: stage });
90
+ };
91
+ const onData = (c) => {
92
+ const s = String(c);
93
+ if (/Download|Resolve|Installed|Prepared/i.test(s)) bump();
94
+ };
95
+ proc.stdout.on('data', onData);
96
+ proc.stderr.on('data', onData);
97
+ proc.on('close', (code) => {
98
+ state.installing = false;
99
+ if (code === 0 && headroomExe()) {
100
+ emitInstallProgress('headroom', { pct: 100 });
101
+ } else {
102
+ state.installFailed = true;
103
+ state.installError = 'kurulum başarısız — elle: uv tool install "headroom-ai[proxy]"';
104
+ }
105
+ _emit();
106
+ /* kurulum bitti ve açık kaldıysa proxy'leri hemen başlat */
107
+ if (state.enabled && headroomExe()) start().catch(() => {});
108
+ });
109
+ proc.on('error', () => {
110
+ state.installing = false;
111
+ state.installFailed = true;
112
+ state.installError = 'kurulum başlatılamadı (uv)';
113
+ _emit();
114
+ });
115
+ }
116
+
117
+ /* ---------- PROXY SÜREÇLERİ ----------
118
+ Provider başına bir proxy; cache modunu BOZMAMAK için mode=token,
119
+ Beast'in tanımadığı headroom_retrieve aracı enjekte EDİLMESİN diye
120
+ no-ccr (geri-getirilemez sıkıştırma, en yüksek tasarruf) ve rate limit
121
+ kapalı (Beast'in kendi istek patlamalarına takılmasın). Beacon kapalı. */
122
+ function nextPort() {
123
+ const used = new Set([...state.proxies.values()].map((p) => p.port));
124
+ for (let i = 0; i < MAX_PROXIES; i++) {
125
+ const port = PORT_BASE + i;
126
+ if (!used.has(port)) return port;
127
+ }
128
+ return 0;
129
+ }
130
+
131
+ function spawnFor(provider) {
132
+ return new Promise((resolve) => {
133
+ const exe = headroomExe();
134
+ if (!exe) return resolve({ ok: false, error: 'headroom kurulu değil' });
135
+ const port = nextPort();
136
+ if (!port) return resolve({ ok: false, error: 'port kalmadı' });
137
+ let proc;
138
+ try {
139
+ proc = spawn(exe, [
140
+ 'proxy', '--port', String(port), '--openai-api-url', provider.baseUrl,
141
+ '--mode', 'token', '--no-ccr', '--no-rate-limit',
142
+ ], {
143
+ windowsHide: true,
144
+ env: { ...process.env, HEADROOM_BEACON: 'off', DO_NOT_TRACK: '1' },
145
+ });
146
+ } catch (e) {
147
+ return resolve({ ok: false, error: String((e && e.message) || e) });
148
+ }
149
+ const rec = { proc, port, baseUrl: provider.baseUrl, ready: false };
150
+ state.proxies.set(provider.id, rec);
151
+ _emit();
152
+ proc.on('exit', () => {
153
+ if (state.proxies.get(provider.id) === rec) state.proxies.delete(provider.id);
154
+ _emit();
155
+ });
156
+ const t0 = Date.now();
157
+ const poll = setInterval(async () => {
158
+ let ready = false;
159
+ try {
160
+ const r = await fetch('http://127.0.0.1:' + port + '/health', { signal: AbortSignal.timeout(2000) });
161
+ ready = !!r.ok;
162
+ } catch {}
163
+ if (ready) {
164
+ clearInterval(poll);
165
+ rec.ready = true;
166
+ _emit();
167
+ return resolve({ ok: true, port });
168
+ }
169
+ if (Date.now() - t0 > 90000) {
170
+ clearInterval(poll);
171
+ try { rec.proc.kill(); } catch {}
172
+ state.proxies.delete(provider.id);
173
+ _emit();
174
+ resolve({ ok: false, error: 'proxy zaman aşımı (90sn)' });
175
+ }
176
+ }, 1200);
177
+ });
178
+ }
179
+
180
+ async function start() {
181
+ const exe = headroomExe();
182
+ if (!exe) {
183
+ autoInstall();
184
+ return { ok: false, error: 'kuruluyor — birkaç dakika sonra hazır olur' };
185
+ }
186
+ state.starting = true;
187
+ _emit();
188
+ try {
189
+ const providers = (state.providerGetter ? state.providerGetter() : []).slice(0, MAX_PROXIES);
190
+ const routes = {};
191
+ /* önce eskileri temizle: listeden çıkan provider'ın proxy'sini kapat */
192
+ const keep = new Set(providers.map((p) => p.id));
193
+ for (const [id, rec] of [...state.proxies.entries()]) {
194
+ if (keep.has(id)) continue;
195
+ try { rec.proc.kill(); } catch {}
196
+ try { spawn('taskkill', ['/pid', String(rec.proc.pid), '/T', '/F'], { windowsHide: true }); } catch {}
197
+ state.proxies.delete(id);
198
+ }
199
+ for (const p of providers) {
200
+ if (!p.baseUrl || state.proxies.has(p.id)) continue;
201
+ const r = await spawnFor(p);
202
+ if (r.ok) routes[p.id] = r.port;
203
+ }
204
+ try { require('./llm').setHeadroomRoutes(routes); } catch {}
205
+ state.enabled = true;
206
+ return { ok: true, routes };
207
+ } finally {
208
+ state.starting = false;
209
+ _emit();
210
+ }
211
+ }
212
+
213
+ async function setEnabled(on) {
214
+ if (on) {
215
+ state.enabled = true;
216
+ if (!headroomExe()) {
217
+ autoInstall();
218
+ return { ok: false, installing: true, error: 'headroom arka planda kuruluyor — birkaç dakika sonra hazır' };
219
+ }
220
+ return start();
221
+ }
222
+ state.enabled = false;
223
+ stopAll();
224
+ try { require('./llm').setHeadroomRoutes(null); } catch {}
225
+ return { ok: true };
226
+ }
227
+
228
+ /* provider listesi değişti (eklendi/silindi) → proxy setini senkronla */
229
+ function syncProviders() {
230
+ if (!state.enabled || !headroomExe()) return;
231
+ start().catch(() => {});
232
+ }
233
+
234
+ function stopAll() {
235
+ for (const rec of state.proxies.values()) {
236
+ try { rec.proc.kill(); } catch {}
237
+ try { spawn('taskkill', ['/pid', String(rec.proc.pid), '/T', '/F'], { windowsHide: true }); } catch {}
238
+ }
239
+ state.proxies.clear();
240
+ _emit();
241
+ }
242
+
243
+ function setProviderGetter(fn) { state.providerGetter = typeof fn === 'function' ? fn : null; }
244
+ function setStatusSink(fn) { state.statusSink = typeof fn === 'function' ? fn : null; }
245
+
246
+ /* proxy /stats özeti (sağlık duvarı yok — hata.yakalanır) */
247
+ async function stats() {
248
+ const rec = [...state.proxies.values()].find((p) => p.ready);
249
+ if (!rec) return { ok: false, running: false };
250
+ try {
251
+ const r = await fetch('http://127.0.0.1:' + rec.port + '/stats', { signal: AbortSignal.timeout(4000) });
252
+ const j = await r.json();
253
+ const s = (j && (j.tokens || j.summary)) || {};
254
+ return {
255
+ ok: true,
256
+ running: true,
257
+ port: rec.port,
258
+ input: Number(s.input) || 0,
259
+ output: Number(s.output) || 0,
260
+ saved: Number(s.saved) || 0,
261
+ proxyCompressionSaved: Number(s.proxy_compression_saved) || 0,
262
+ savingsPercent: Number(s.savings_percent) || 0,
263
+ allLayersSaved: Number(s.all_layers_saved) || 0,
264
+ };
265
+ } catch {
266
+ return { ok: false, running: true, port: rec.port };
267
+ }
268
+ }
269
+
270
+ module.exports = {
271
+ probe,
272
+ autoInstall,
273
+ setEnabled,
274
+ syncProviders,
275
+ stopAll,
276
+ setProviderGetter,
277
+ setStatusSink,
278
+ stats,
279
+ };