beast-agent 2.34.0 → 2.35.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.34.0",
4
+ "version": "2.35.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -25,6 +25,7 @@ const mcp = require('./mcp');
25
25
  const apps = require('./apps');
26
26
  const pdfwrite = require('./pdfwrite');
27
27
  const { estTokens, estMsgTokens } = require('./tokens');
28
+ const squeeze = require('./squeeze');
28
29
  const log = require('./logger');
29
30
  /* opencode mantığı (birebir port): ajanlar, izinler, tool seti, promptlar —
30
31
  Beast Code (bcCode) oturumları bu çekirdek üzerinden koşar */
@@ -767,6 +768,12 @@ class Engine {
767
768
  413/TPM "Requested N" hatası → gerçek token sayısı öğrenilir, tahmin
768
769
  kalibre edilir ve aynı model BİR KEZ daha sıkı sıkıştırmayla denenir. */
769
770
  async _streamWithFallbacks(session, payload, activeTools, signal, onDelta, sel, wasVision) {
771
+ /* SQUEEZE (varsayılan kapalı): istek kopyası sıkıştırılır — oturum kaydı
772
+ değişmez. Provider limiti kontrolünden ÖNCE uygulanır ki limit hesabı
773
+ küçülmüş gövdeyi görsün. */
774
+ if (squeeze.isEnabled()) {
775
+ payload = squeeze.apply(payload, { cacheKey: String(session.id || '') });
776
+ }
770
777
  const cands = this._chatCandidates(sel, wasVision);
771
778
  let lastErr = null;
772
779
  for (let i = 0; i < cands.length; i++) {
@@ -4773,10 +4780,14 @@ const skills = require('./skills');
4773
4780
  ];
4774
4781
  try {
4775
4782
  for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
4783
+ let sendMsgs = [{ role: 'system', content: system }, ...msgs.slice(1)];
4784
+ if (squeeze.isEnabled()) {
4785
+ sendMsgs = squeeze.apply(sendMsgs, { cacheKey: String(sessionId || 'subagent') });
4786
+ }
4776
4787
  const res = await chatStreamAuto(
4777
4788
  sel,
4778
4789
  {
4779
- messages: [{ role: 'system', content: system }, ...msgs.slice(1)],
4790
+ messages: sendMsgs,
4780
4791
  tools: subTools,
4781
4792
  reasoningEffort: this._thinkEffort(),
4782
4793
  cacheKey: String(sessionId || 'subagent'),
package/src/agent/llm.js CHANGED
@@ -347,18 +347,6 @@ function sessionHeaderId(body) {
347
347
  return _sessionFallback;
348
348
  }
349
349
 
350
- /* Headroom proxy yönlendirme: providerId -> yerel proxy portu haritası.
351
- main (agent/headroom.js) ayarlardan açıldığında kurar; null = düz bağlan. */
352
- let _headroomRoutes = null;
353
- function setHeadroomRoutes(map) {
354
- _headroomRoutes = map && Object.keys(map).length ? map : null;
355
- }
356
- function headroomUrlFor(sel) {
357
- if (!_headroomRoutes || !sel) return '';
358
- const port = Number(_headroomRoutes[String(sel.providerId || '')] || 0);
359
- return port ? 'http://127.0.0.1:' + port + '/v1/chat/completions' : '';
360
- }
361
-
362
350
  async function openChat(sel, body, { stream, signal, drops } = {}) {
363
351
  const d = drops || {};
364
352
  const payload = {
@@ -380,9 +368,7 @@ async function openChat(sel, body, { stream, signal, drops } = {}) {
380
368
  if (body.cacheKey && wantsCacheKey(sel) && !d.noCacheKey) {
381
369
  payload.prompt_cache_key = String(body.cacheKey);
382
370
  }
383
- /* Headroom açıkken istek yerel sıkıştırma proxy'sinden geçer (aynı gövde+başlıklar);
384
- yoksa sağlayıcıya düz bağlan */
385
- const target = headroomUrlFor(sel) || sel.url;
371
+ const target = sel.url;
386
372
  return fetch(target, {
387
373
  method: 'POST',
388
374
  signal,
@@ -641,7 +627,6 @@ module.exports = {
641
627
  capsFor,
642
628
  resetCaps,
643
629
  learnCaps,
644
- setHeadroomRoutes,
645
630
  PARAM_LABELS,
646
631
  isNetworkError,
647
632
  wantsCacheKey,
@@ -0,0 +1,312 @@
1
+ 'use strict';
2
+
3
+ /* Beast SQUEEZE — kendi bağlam (token) sıkıştırıcımız. VARSAYILAN KAPALI.
4
+ Her LLM isteğinden hemen önce mesaj dizisinin KOPYASINA uygulanır; oturum
5
+ kaydı/geçmiş DEĞİŞMEZ. Harici kurulum/proxy yok.
6
+
7
+ Kurallar:
8
+ 1) TEKRAR BLOK: Aynı büyük içerik (araç çıktısı / dosya eki / skill gövdesi)
9
+ aynı bağlamda birden çok kez varsa YALNIZ İLK kopya tam gider; sonrakiler
10
+ kısa işaretçiye iner. İlk kopya bağlamdan düşerse (compaction/bütçe) sonraki
11
+ kopya kendiliğinden yeniden TAM gönderilir — "bağlam koptuysa yeniden" kuralı.
12
+ 2) ESKİ ARAÇ ÇIKTISI: Son N araç sonucu birebir; daha eskiler başlık + baş/son
13
+ önizleme + "gerekirse tekrar çalıştır/oku" notu olarak sıkıştırılır.
14
+ Her dosyanın EN SON read_file sonucu asla sıkıştırılmaz (dosya hafızası).
15
+ 3) SKILL: Aynı skill gövdesi tekrarlıysa eski kopyalar stub'a iner; tek kopya tam.
16
+ 4) DOKUNULMAZLAR: system, son tur birimleri, assistant mesajları, zaten
17
+ sıkıştırılmış içerik (idempotans) ve minBlockChars'tan küçük bloklar. */
18
+
19
+ const crypto = require('crypto');
20
+ const { estMsgTokens } = require('./tokens');
21
+
22
+ const DEFAULTS = {
23
+ minTotalTokens: 1500,
24
+ minBlockChars: 700,
25
+ minUserCompactChars: 12000,
26
+ keepRecentTools: 0,
27
+ keepRecentUnits: 4,
28
+ protectReadsWindow: 8,
29
+ previewHead: 700,
30
+ previewTail: 250,
31
+ };
32
+
33
+ const MARK = '[squeeze:';
34
+
35
+ const state = {
36
+ enabled: false,
37
+ stats: {
38
+ calls: 0,
39
+ changedCalls: 0,
40
+ savedTokens: 0,
41
+ lastBeforeTokens: 0,
42
+ lastAfterTokens: 0,
43
+ lastSavedTokens: 0,
44
+ deduped: 0,
45
+ compactedTools: 0,
46
+ compactedSkills: 0,
47
+ compactedUsers: 0,
48
+ },
49
+ };
50
+
51
+ function setEnabled(on) {
52
+ state.enabled = !!on;
53
+ }
54
+
55
+ function isEnabled() {
56
+ return state.enabled;
57
+ }
58
+
59
+ function resetStats() {
60
+ for (const k of Object.keys(state.stats)) state.stats[k] = 0;
61
+ }
62
+
63
+ function getStats() {
64
+ const s = { ...state.stats };
65
+ s.lastPercent =
66
+ s.lastBeforeTokens > 0 ? Math.round((s.lastSavedTokens / s.lastBeforeTokens) * 100) : 0;
67
+ return s;
68
+ }
69
+
70
+ function _hash(s) {
71
+ try {
72
+ return crypto.createHash('sha1').update(s).digest('hex').slice(0, 16);
73
+ } catch {
74
+ return String(s.length) + ':' + String(s.charCodeAt(0) || 0);
75
+ }
76
+ }
77
+
78
+ function _isSqueezed(content) {
79
+ return typeof content === 'string' && content.includes(MARK);
80
+ }
81
+
82
+ /* assistant(tool_calls) + ardışık tool sonuçlarını BÖLMEYEN birimler (indis) */
83
+ function _indexUnits(messages) {
84
+ const units = [];
85
+ let i = 0;
86
+ while (i < messages.length) {
87
+ const m = messages[i];
88
+ if (m && m.role === 'assistant' && m.tool_calls && m.tool_calls.length) {
89
+ const grp = [i];
90
+ i++;
91
+ while (i < messages.length && messages[i] && messages[i].role === 'tool') grp.push(i++);
92
+ units.push(grp);
93
+ } else {
94
+ units.push([i]);
95
+ i++;
96
+ }
97
+ }
98
+ return units;
99
+ }
100
+
101
+ /* tool_call_id → {name, args} (assistant mesajlarındaki çağrılardan) */
102
+ function _toolCallMap(messages) {
103
+ const map = new Map();
104
+ for (const m of messages) {
105
+ if (!m || m.role !== 'assistant' || !Array.isArray(m.tool_calls)) continue;
106
+ for (const tc of m.tool_calls) {
107
+ if (!tc || !tc.id) continue;
108
+ let args = {};
109
+ try {
110
+ args = JSON.parse((tc.function && tc.function.arguments) || '{}');
111
+ } catch {}
112
+ map.set(String(tc.id), {
113
+ name: String((tc.function && tc.function.name) || ''),
114
+ args,
115
+ });
116
+ }
117
+ }
118
+ return map;
119
+ }
120
+
121
+ function _readTarget(args) {
122
+ if (!args || typeof args !== 'object') return '';
123
+ for (const k of ['file_path', 'path', 'file', 'filename', 'filePath', 'target']) {
124
+ if (args[k]) return String(args[k]);
125
+ }
126
+ return '';
127
+ }
128
+
129
+ function _pointerFor(role, name) {
130
+ if (role === 'tool') {
131
+ return (
132
+ MARK + ' bu araç çıktısı yukarıdaki' + (name ? " '" + name + "'" : '') +
133
+ ' sonucuyla BİREBİR AYNI — tekrar gönderilmedi]'
134
+ );
135
+ }
136
+ return MARK + ' bu içerik yukarıda TAM gönderildi — tekrar gönderilmedi]';
137
+ }
138
+
139
+ function _skillStub(raw) {
140
+ let name = '';
141
+ try {
142
+ const j = JSON.parse(raw);
143
+ name = String(j.name || j.skill || '');
144
+ } catch {}
145
+ return (
146
+ '{"ok":true,"name":"' + name.replace(/"/g, '') + '",' +
147
+ '"content":"' + MARK + ' bu skill gövdesi yukarıda tam yüklendi — tekrar gönderilmedi]"}'
148
+ );
149
+ }
150
+
151
+ function _compactPreview(content, cfg, label, hint) {
152
+ const len = content.length;
153
+ const headLen = Math.min(cfg.previewHead, Math.max(260, Math.floor(len * 0.4)));
154
+ const tailLen = len - headLen > 400 ? Math.min(cfg.previewTail, Math.floor(len * 0.15)) : 0;
155
+ const head = content.slice(0, headLen);
156
+ const tail = tailLen > 0 ? content.slice(-tailLen) : '';
157
+ const note = '\n' + MARK + ' ' + label + ' sıkıştırıldı: ' + len + ' karakter → önizleme. ' + hint + ']\n';
158
+ return tail ? head + note + tail : head + note;
159
+ }
160
+
161
+ /* messages: OpenAI-uyumlu mesaj dizisi. Dönüş: (gerekirse) yeni dizi.
162
+ Kapalıyken aynı referans döner; açıkken de değişiklik yoksa orijinal döner. */
163
+ function apply(messages, opts = {}) {
164
+ if (!state.enabled || !Array.isArray(messages) || messages.length < 2) return messages;
165
+
166
+ const cfg = { ...DEFAULTS, ...(opts.config || {}) };
167
+ const beforeTokens = messages.reduce((a, m) => a + estMsgTokens(m), 0);
168
+ state.stats.calls++;
169
+ state.stats.lastBeforeTokens = beforeTokens;
170
+ state.stats.lastAfterTokens = beforeTokens;
171
+ state.stats.lastSavedTokens = 0;
172
+ if (beforeTokens < cfg.minTotalTokens) return messages;
173
+
174
+ const out = messages.slice();
175
+ let touched = false;
176
+ const replace = (i, msg) => {
177
+ out[i] = msg;
178
+ touched = true;
179
+ };
180
+
181
+ const units = _indexUnits(messages);
182
+ const protectFrom = Math.max(1, units.length - cfg.keepRecentUnits);
183
+ const protectedIdx = new Set([0]);
184
+ for (let u = protectFrom; u < units.length; u++) {
185
+ for (const i of units[u]) protectedIdx.add(i);
186
+ }
187
+ /* son N araç sonucu birebir kalır */
188
+ const toolIdx = [];
189
+ for (let i = 0; i < messages.length; i++) {
190
+ if (messages[i] && messages[i].role === 'tool') toolIdx.push(i);
191
+ }
192
+ if (cfg.keepRecentTools > 0) {
193
+ for (const i of toolIdx.slice(-cfg.keepRecentTools)) protectedIdx.add(i);
194
+ }
195
+
196
+ const meta = _toolCallMap(messages);
197
+ const toolName = (i) => {
198
+ const m = messages[i];
199
+ const fromCall = m && m.tool_call_id ? meta.get(String(m.tool_call_id)) : null;
200
+ return String((m && m.name) || (fromCall && fromCall.name) || '');
201
+ };
202
+
203
+ /* her dosyanın EN SON okuması, yakın penceredeyse korunur */
204
+ const protectReadsFrom =
205
+ toolIdx.length > cfg.protectReadsWindow
206
+ ? toolIdx[toolIdx.length - cfg.protectReadsWindow]
207
+ : (toolIdx[0] ?? Infinity);
208
+ const latestRead = new Map();
209
+ for (const i of toolIdx) {
210
+ const m = messages[i];
211
+ if (!m || String(m.name || '') !== 'read_file') continue;
212
+ const info = m.tool_call_id ? meta.get(String(m.tool_call_id)) : null;
213
+ const target = _readTarget(info && info.args) || '__last__';
214
+ latestRead.set(target, i);
215
+ }
216
+
217
+ let deduped = 0;
218
+ let compactedTools = 0;
219
+ let compactedSkills = 0;
220
+ let compactedUsers = 0;
221
+
222
+ /* 1) SKILL gövde tekrarı: aynı skill'in eski kopyaları stub'a iner */
223
+ const skillSeen = new Set();
224
+ for (let i = 0; i < messages.length; i++) {
225
+ const m = messages[i];
226
+ if (!m || m.role !== 'tool' || toolName(i) !== 'skill') continue;
227
+ if (typeof m.content !== 'string' || _isSqueezed(m.content)) continue;
228
+ let key = '';
229
+ try {
230
+ const j = JSON.parse(m.content);
231
+ key = String(j.name || j.skill || '');
232
+ } catch {}
233
+ if (!key) key = _hash(m.content);
234
+ if (skillSeen.has(key)) {
235
+ replace(i, { ...m, content: _skillStub(m.content) });
236
+ compactedSkills++;
237
+ } else {
238
+ skillSeen.add(key);
239
+ }
240
+ }
241
+
242
+ /* 2) GENEL tekrar bloğu: ilk kopya tam, sonrakiler işaretçi (korumalılar hariç) */
243
+ const seen = new Map();
244
+ for (let i = 0; i < messages.length; i++) {
245
+ const m = messages[i];
246
+ if (!m || m.role === 'system' || m.role === 'assistant') continue;
247
+ if (typeof m.content !== 'string') continue;
248
+ if (m.content.length < cfg.minBlockChars || _isSqueezed(m.content)) continue;
249
+ const role = m.role === 'tool' ? 'tool' : 'user';
250
+ const key = role + ':' + _hash(m.content);
251
+ if (seen.has(key)) {
252
+ /* birebir aynı içerik: en son mesaj (güncel tur) hariç her yerde
253
+ işaretçiye inebilir — içerik zaten aynı bağlamda tam duruyor */
254
+ if (i === messages.length - 1) continue;
255
+ replace(i, { ...out[i], content: _pointerFor(role, toolName(i)) });
256
+ deduped++;
257
+ } else {
258
+ seen.set(key, i);
259
+ }
260
+ }
261
+
262
+ /* 3) ESKİ ARAÇ ÇIKTISI: başlık + baş/son önizleme */
263
+ for (const i of toolIdx) {
264
+ if (protectedIdx.has(i)) continue;
265
+ const m = out[i];
266
+ if (!m || typeof m.content !== 'string' || _isSqueezed(m.content)) continue;
267
+ if (m.content.length < cfg.minBlockChars) continue;
268
+ const name = toolName(i);
269
+ if (name === 'skill') continue;
270
+ if (name === 'read_file') {
271
+ const info = m.tool_call_id ? meta.get(String(m.tool_call_id)) : null;
272
+ const target = _readTarget(info && info.args) || '__last__';
273
+ if (latestRead.get(target) === i && i >= protectReadsFrom) continue;
274
+ }
275
+ const hint =
276
+ name === 'read_file'
277
+ ? 'Tam içerik diskte duruyor; gerekirse read_file ile tekrar oku'
278
+ : 'Gerekirse ' + (name || 'araç') + ' ile tekrar çalıştır';
279
+ replace(i, { ...m, content: _compactPreview(m.content, cfg, "'" + (name || 'araç') + "'", hint) });
280
+ compactedTools++;
281
+ }
282
+
283
+ /* 4) ESKİ BÜYÜK KULLANICI İÇERİĞİ (dosya eki vb.): baş/son önizleme */
284
+ for (let i = 0; i < messages.length; i++) {
285
+ const m = messages[i];
286
+ if (!m || m.role !== 'user' || protectedIdx.has(i) || i === 0) continue;
287
+ if (typeof m.content !== 'string' || _isSqueezed(m.content)) continue;
288
+ if (m.content.length < cfg.minUserCompactChars) continue;
289
+ if (m.content.startsWith('[ÖNCEKİ KONUŞMANIN ÖZETİ')) continue;
290
+ replace(i, {
291
+ ...m,
292
+ content: _compactPreview(m.content, cfg, 'kullanıcı içeriği', 'Tam içerik oturum kaydında duruyor'),
293
+ });
294
+ compactedUsers++;
295
+ }
296
+
297
+ if (!touched) return messages;
298
+
299
+ const afterTokens = out.reduce((a, m) => a + estMsgTokens(m), 0);
300
+ const saved = Math.max(0, beforeTokens - afterTokens);
301
+ state.stats.lastAfterTokens = afterTokens;
302
+ state.stats.lastSavedTokens = saved;
303
+ state.stats.savedTokens += saved;
304
+ state.stats.deduped += deduped;
305
+ state.stats.compactedTools += compactedTools;
306
+ state.stats.compactedSkills += compactedSkills;
307
+ state.stats.compactedUsers += compactedUsers;
308
+ if (saved > 0) state.stats.changedCalls++;
309
+ return out;
310
+ }
311
+
312
+ module.exports = { apply, setEnabled, isEnabled, getStats, resetStats, MARK };
package/src/main.js CHANGED
@@ -28,7 +28,7 @@ const bus = require('./agent/bus');
28
28
  const computeruse = require('./agent/computeruse');
29
29
  const fsguard = require('./agent/fsguard');
30
30
  const log = require('./agent/logger');
31
- const headroom = require('./agent/headroom');
31
+ const squeeze = require('./agent/squeeze');
32
32
  const QRCode = require('qrcode'); /* Expo Go QR (bc-expurl) — whatsapp ile aynı paket */
33
33
 
34
34
  /* Renderer'a sır gönderirken kullanılan maske; kaydederken aynen geri gelirse
@@ -3465,29 +3465,9 @@ function reloadBackend() {
3465
3465
  süreç koşucusu (sandbox:run IPC ile aynı makine). */
3466
3466
  engine.sbRunHook = sbRunStartManaged;
3467
3467
 
3468
- /* HEADROOM (opsiyonel token sıkıştırma proxy'si): engine zincirinden provider
3469
- listesi alınır; durum değişince renderer'a 'headroom-status' düşer */
3470
- headroom.setProviderGetter(() => {
3471
- const seen = new Map();
3472
- for (const c of (engine && engine.cfg && engine.cfg.chain) || []) {
3473
- if (!c || !c.providerId || !c.url || seen.has(c.providerId)) continue;
3474
- seen.set(c.providerId, {
3475
- id: String(c.providerId),
3476
- name: String(c.providerName || c.providerId),
3477
- baseUrl: String(c.url).replace(/\/chat\/completions$/, ''),
3478
- });
3479
- }
3480
- return [...seen.values()];
3481
- });
3482
- headroom.setStatusSink((st) => {
3483
- try {
3484
- if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'headroom-status', status: st });
3485
- } catch {}
3486
- });
3487
- /* ayarlardan açık kaldıysa: uygulama açılışında arka planda başlat */
3488
- if (settings.headroom && settings.headroom.enabled) {
3489
- setTimeout(() => { headroom.setEnabled(true).catch(() => {}); }, 4000);
3490
- }
3468
+ /* SQUEEZE (opsiyonel token sıkıştırma): varsayılan KAPALI; ayar açıksa
3469
+ her LLM isteğinin kopyası gönderimden önce sıkıştırılır. */
3470
+ squeeze.setEnabled(!!(settings.squeeze && settings.squeeze.enabled));
3491
3471
  return engine.publicState();
3492
3472
  }
3493
3473
 
@@ -3718,7 +3698,6 @@ app.whenReady().then(() => {
3718
3698
  flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
3719
3699
  try { toolsMod.disposeShellSessions(); } catch {} // kalıcı shell oturumlarını kapat
3720
3700
  try { require('./agent/mcp').stopAll(); } catch {} // MCP server süreçlerini kapat
3721
- try { headroom.stopAll(); } catch {} // headroom proxy süreçlerini kapat
3722
3701
  });
3723
3702
 
3724
3703
  if (process.argv.includes('--smoke')) {
@@ -6673,44 +6652,24 @@ ipcMain.handle('install:status', async () => {
6673
6652
  /* 8) Edge TTS (bulut) */
6674
6653
  rows.push({ id: 'edge', name: 'Edge TTS — seslendirme', state: 'cloud', detail: 'bulut — kurulum gerekmez' });
6675
6654
 
6676
- /* 9) Headroom CLI — arka plan paket kurulumu burada görünür;
6677
- aç/kapat Token Sıkıştırma sekmesinde */
6678
- {
6679
- const st = headroom.probe();
6680
- let state = 'optional';
6681
- if (st.installed) state = 'ok';
6682
- else if (st.installing) state = 'loading';
6683
- if (st.failed) state = 'failed';
6684
- rows.push({
6685
- id: 'headroom',
6686
- name: 'Headroom CLI — token sıkıştırma motoru',
6687
- state,
6688
- detail: st.installing
6689
- ? 'arka planda kuruluyor — uv tool install headroom-ai[proxy]'
6690
- : st.failed
6691
- ? (st.error || 'kurulum başarısız')
6692
- : st.installed
6693
- ? 'kurulu — aç/kapat: Token Sıkıştırma sekmesi'
6694
- : 'kurulu değil — Token Sıkıştırma sekmesinden açınca otomatik kurulur',
6695
- ...(st.installing ? pctFields('headroom') : {}),
6696
- });
6697
- }
6698
-
6699
6655
  return rows;
6700
6656
  });
6701
6657
 
6702
6658
  /* cua-driver kurulumunu şimdi başlat (Kurulum sekmesi otomatiği + elle tetikleme) */
6703
6659
  ipcMain.handle('cua:install', () => require('./agent/computeruse').autoInstall());
6704
6660
 
6705
- /* HEADROOM (opsiyonel token sıkıştırma): aç/kapa + durum */
6706
- ipcMain.handle('headroom:toggle', async (_e, on) => {
6707
- settings.headroom = { ...(settings.headroom || {}), enabled: !!on };
6661
+ /* SQUEEZE (kendi token sıkıştırıcımız): aç/kapa + durum + istatistik */
6662
+ ipcMain.handle('squeeze:toggle', (_e, on) => {
6663
+ settings.squeeze = { ...(settings.squeeze || {}), enabled: !!on };
6708
6664
  saveSettings();
6709
- const r = await headroom.setEnabled(!!on).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
6710
- return r;
6665
+ squeeze.setEnabled(!!on);
6666
+ return { ok: true, enabled: !!on };
6667
+ });
6668
+ ipcMain.handle('squeeze:status', () => ({ enabled: squeeze.isEnabled(), stats: squeeze.getStats() }));
6669
+ ipcMain.handle('squeeze:reset', () => {
6670
+ squeeze.resetStats();
6671
+ return { ok: true, stats: squeeze.getStats() };
6711
6672
  });
6712
- ipcMain.handle('headroom:status', () => headroom.probe());
6713
- ipcMain.handle('headroom:stats', () => headroom.stats());
6714
6673
 
6715
6674
  /* ANDROID EMÜLATÖR: kaldırıldı — mobil önizleme tunnel (telefon QR) yoluyla yapılır */
6716
6675
 
@@ -11083,7 +11042,6 @@ ipcMain.handle('custom:set', (_e, list) => {
11083
11042
  });
11084
11043
  saveSettings();
11085
11044
  engine.setCustomProviders(settings.customProviders);
11086
- headroom.syncProviders(); /* headroom açıksa proxy setini yeni listeye uydur */
11087
11045
  return engine.publicState();
11088
11046
  });
11089
11047
 
package/src/preload.js CHANGED
@@ -260,9 +260,9 @@ contextBridge.exposeInMainWorld('beast', {
260
260
  idePreviewUrl: (url) => ipcRenderer.invoke('ide:previewUrl', url),
261
261
  ideProjInfo: () => ipcRenderer.invoke('ide:projinfo'),
262
262
  ideExpQr: () => ipcRenderer.invoke('ide:expqr'),
263
- headroomToggle: (on) => ipcRenderer.invoke('headroom:toggle', !!on),
264
- headroomStatus: () => ipcRenderer.invoke('headroom:status'),
265
- headroomStats: () => ipcRenderer.invoke('headroom:stats'),
263
+ squeezeToggle: (on) => ipcRenderer.invoke('squeeze:toggle', !!on),
264
+ squeezeStatus: () => ipcRenderer.invoke('squeeze:status'),
265
+ squeezeReset: () => ipcRenderer.invoke('squeeze:reset'),
266
266
  agentsClearAll: () => ipcRenderer.invoke('agents:clearAll'),
267
267
  ideDelete: (rel) => ipcRenderer.invoke('ide:delete', rel),
268
268
  onWaEvent: (cb) => ipcRenderer.on('wa:event', (_e, ev) => cb(ev)),
@@ -66,7 +66,7 @@
66
66
  set_title: 'AYARLAR',
67
67
  tab_lang: 'Dil',
68
68
  lang_sub: 'Arayüz dili — uygulama dili ve sesli komut dili buradan değişir',
69
- tab_headroom: 'Token Sıkıştırma',
69
+ tab_squeeze: 'Squeeze',
70
70
  tab_provider: 'Provider',
71
71
  tab_fallout: 'Fallout',
72
72
  tab_memory: 'Memory',
@@ -787,7 +787,7 @@
787
787
  set_title: 'SETTINGS',
788
788
  tab_lang: 'Language',
789
789
  lang_sub: 'Interface language — app UI and voice command language',
790
- tab_headroom: 'Token Compression',
790
+ tab_squeeze: 'Squeeze',
791
791
  tab_provider: 'Provider',
792
792
  tab_fallout: 'Fallout',
793
793
  tab_memory: 'Memory',
@@ -511,7 +511,7 @@
511
511
  <button class="tab" data-tab="cron" data-i18n="tab_cron">Cron</button>
512
512
  <button class="tab" data-tab="agenda" data-i18n="tab_agenda">Ajanda</button>
513
513
  <button class="tab" data-tab="usage" data-i18n="tab_usage">Maliyet · Limit</button>
514
- <button class="tab" data-tab="headroom" data-i18n="tab_headroom">Token Sıkıştırma</button>
514
+ <button class="tab" data-tab="squeeze" data-i18n="tab_squeeze">Squeeze</button>
515
515
  <button class="tab" data-tab="logs" data-i18n="tab_logs">Log</button>
516
516
  <button class="tab" data-tab="dash" data-i18n="tab_dash">Dashboard</button>
517
517
  <button class="tab" data-tab="sec" data-i18n="tab_security">Güvenlik</button>
@@ -537,7 +537,7 @@
537
537
  <div id="tab-events" class="pane" hidden></div>
538
538
  <div id="tab-empati" class="pane" hidden></div>
539
539
  <div id="tab-usage" class="pane" hidden></div>
540
- <div id="tab-headroom" class="pane" hidden></div>
540
+ <div id="tab-squeeze" class="pane" hidden></div>
541
541
  <div id="tab-logs" class="pane" hidden></div>
542
542
  <div id="tab-dash" class="pane" hidden></div>
543
543
  <div id="tab-sec" class="pane" hidden></div>
@@ -1213,7 +1213,6 @@ function addFiles(files, mode) {
1213
1213
  /* ---------------- settings ---------------- */
1214
1214
 
1215
1215
  let setTab = 'lang'; /* ayarlar artık DİL sekmesiyle açılır */
1216
- let hrPaneTimer = null; /* headroom-status → Kurulum sekmesi debounce tazeleme */
1217
1216
 
1218
1217
  /* DİL sekmesi: arayüz dili seçimi (eski sağ-alt dil ikonunun yerine) */
1219
1218
  function renderLangPane() {
@@ -1280,7 +1279,7 @@ async function renderActiveSettingsTab() {
1280
1279
  case 'cron': await openCron(); break;
1281
1280
  case 'agenda': await renderAgendaPane(); break;
1282
1281
  case 'usage': await renderUsagePane(); break;
1283
- case 'headroom': await renderHeadroomPane(); break;
1282
+ case 'squeeze': await renderSqueezePane(); break;
1284
1283
  case 'agents': await refreshAgentsPane(); break;
1285
1284
  case 'logs': await renderLogPane(); break;
1286
1285
  case 'dash': await renderDashboardPane(); break;
@@ -1364,7 +1363,7 @@ function switchTab(name) {
1364
1363
  document.querySelectorAll('#setTabs .tab').forEach((b) =>
1365
1364
  b.classList.toggle('active', b.dataset.tab === name)
1366
1365
  );
1367
- for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'agenda', 'usage', 'headroom', 'logs', 'dash', 'sec', 'update']) {
1366
+ for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'agenda', 'usage', 'squeeze', 'logs', 'dash', 'sec', 'update']) {
1368
1367
  const el = $('#tab-' + p);
1369
1368
  if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
1370
1369
  }
@@ -1374,7 +1373,7 @@ function switchTab(name) {
1374
1373
  if (name === 'email') renderEmailPane();
1375
1374
  if (name === 'usage') renderUsagePane();
1376
1375
  if (name === 'install') renderInstallPane();
1377
- if (name === 'headroom') renderHeadroomPane();
1376
+ if (name === 'squeeze') renderSqueezePane();
1378
1377
  if (name === 'events') renderEventsPane();
1379
1378
  if (name === 'empati') renderEmpatiPane();
1380
1379
  if (name === 'logs') renderLogPane();
@@ -2332,91 +2331,90 @@ function ttsFlushTail() {
2332
2331
  /* ---------------- KURULUM SEKMESİ: bileşenler OTOMATİK kurulur ----------------
2333
2332
  Eksik/kısmen inmiş bileşen sekme açılınca kendiliğinden indirilir; kullanıcı
2334
2333
  hiçbir düğmeye basmaz. Durum canlı güncellenir (4 sn'de bir). */
2335
- /* ---------- TOKEN SIKIŞTIRMA (Headroom) sekmesi ----------
2336
- Opsiyonel yerel sıkıştırma proxy'si: açıkken LLM girdisi %30-70 küçülür.
2337
- Aç/Kapat buradan; CLI kurulumu Kurulum sekmesinde görülür. */
2338
- let hrStatsTimer = null;
2339
-
2340
- async function renderHeadroomPane() {
2341
- const pane = $('#tab-headroom');
2334
+ /* ---------- SQUEEZE (kendi token sıkıştırıcımız) sekmesi ----------
2335
+ Varsayılan KAPALI. Açıkken her LLM isteğinin kopyası gönderimden önce
2336
+ sıkıştırılır: tekrar eden büyük bloklar bağlamda bir kez gider, eski araç
2337
+ çıktıları başlık + önizlemeye iner. Oturum kaydı değişmez. */
2338
+ let sqStatsTimer = null;
2339
+
2340
+ async function renderSqueezePane() {
2341
+ const pane = $('#tab-squeeze');
2342
2342
  if (!pane) return;
2343
- const st = await beast.headroomStatus().catch(() => null);
2344
-
2343
+ const st = await beast.squeezeStatus().catch(() => null);
2344
+ const on = !!(st && st.enabled);
2345
+ const s = (st && st.stats) || {};
2345
2346
  const fmtN = (n) => new Intl.NumberFormat('tr-TR').format(Math.round(Number(n) || 0));
2346
- let stHtml = '';
2347
- if (st) {
2348
- const readyN = (st.proxies || []).filter((p) => p.ready).length;
2349
- let stateTxt;
2350
- if (st.installing) stateTxt = '⏳ arka planda kuruluyor — uv tool install headroom-ai[proxy] (birkaç dakika)';
2351
- else if (st.enabled && readyN) stateTxt = '🟢 açık — proxy: ' + (st.proxies || []).filter((p) => p.ready).map((p) => '127.0.0.1:' + p.port).join(', ');
2352
- else if (st.enabled) stateTxt = '⏳ proxy başlatılıyor… (ilk açılış ~30 sn)';
2353
- else if (st.failed) stateTxt = '⛔ ' + (st.error || 'kurulum başarısız');
2354
- else if (st.installed) stateTxt = '⚪ kapalı — kurulu, aşağıdaki düğmeyle etkinleştir';
2355
- else stateTxt = '⚪ kapalı — açınca uv ile otomatik kurulur (birkaç dakika)';
2356
- stHtml = '<div class="sub" style="margin-top:10px;font-size:12px;text-align:center">' + escapeHtml(stateTxt) + '</div>';
2357
- }
2358
-
2359
- const stats = await beast.headroomStats().catch(() => null);
2347
+
2348
+ const stHtml =
2349
+ '<div class="sub" style="margin-top:10px;font-size:12px;text-align:center">' +
2350
+ (on
2351
+ ? '🟢 açık — her isteğin kopyası gönderimden önce sıkıştırılır; oturum kaydın değişmez'
2352
+ : '⚪ kapalı (varsayılan) — açınca yeni isteklerde uygulanır, geçmişe dokunmaz') +
2353
+ '</div>';
2354
+
2360
2355
  let statsHtml = '';
2361
- if (stats && stats.ok && (stats.input || stats.saved)) {
2356
+ if (s.calls) {
2362
2357
  statsHtml =
2363
2358
  '<div class="divider"></div>' +
2364
- '<h2 style="text-align:center">Tasarruf (proxy açıkken)</h2>' +
2359
+ '<h2 style="text-align:center">Tasarruf</h2>' +
2365
2360
  '<div style="display:flex;gap:24px;flex-wrap:wrap;margin-top:8px;justify-content:center">' +
2366
- '<div><div style="font-size:20px;font-weight:800;color:var(--ok)">%' + Math.round(stats.savingsPercent || 0) + '</div><div class="sub">sıkıştırma oranı</div></div>' +
2367
- '<div><div style="font-size:20px;font-weight:800">' + new Intl.NumberFormat('tr-TR').format(stats.proxyCompressionSaved || 0) + '</div><div class="sub">kaydedilen token</div></div>' +
2368
- '<div><div style="font-size:20px;font-weight:800">' + new Intl.NumberFormat('tr-TR').format(stats.input || 0) + '</div><div class="sub">toplam girdi token</div></div>' +
2369
- '</div>';
2361
+ '<div title="Son istekte tahmini token tasarrufu"><div style="font-size:20px;font-weight:800;color:var(--ok)">%' + (s.lastPercent || 0) + '</div><div class="sub">son istekte sıkıştırma</div></div>' +
2362
+ '<div><div style="font-size:20px;font-weight:800">' + fmtN(s.savedTokens) + '</div><div class="sub">toplam kaydedilen token (tahmini)</div></div>' +
2363
+ '<div><div style="font-size:20px;font-weight:800">' + fmtN(s.changedCalls) + '</div><div class="sub">sıkıştırılan istek</div></div>' +
2364
+ '</div>' +
2365
+ '<div class="sub" style="text-align:center;margin-top:8px">tekrar bloğu: ' + fmtN(s.deduped) +
2366
+ ' · eski araç çıktısı: ' + fmtN(s.compactedTools) +
2367
+ ' · skill: ' + fmtN(s.compactedSkills) +
2368
+ ' · kullanıcı içeriği: ' + fmtN(s.compactedUsers) + '</div>';
2370
2369
  }
2371
2370
 
2372
- /* AÇ/KAPAT düğmesi: ortalanmış + SVG ikonlu (play/pause — uygulama ikon stili) */
2373
- const svgPlay =
2374
- '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="6 3 20 12 6 21 6 3"/></svg>';
2375
- const svgPause =
2376
- '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>';
2377
2371
  pane.innerHTML =
2378
- '<h2 style="text-align:center">' + _t('tab_headroom') + '</h2>' +
2379
- '<div class="sub" style="text-align:center;max-width:620px;margin:0 auto">Headroom yerel proxy\u2019si — araç çıktıları, loglar ve JSON yükleri modele gitmeden sıkıştırılır; orijinali yerelde saklanır. Token faturasını %30-70 düşürür. Yalnız OpenAI-uyumlu sağlayıcı trafiğini etkiler; kapalıyken hiçbir şey değişmez.</div>' +
2372
+ '<h2 style="text-align:center">' + _t('tab_squeeze') + '</h2>' +
2380
2373
  '<div style="display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:18px">' +
2381
- '<button id="hrToggle" class="btn" style="width:auto;padding:11px 16px;display:inline-flex;align-items:center;gap:8px;justify-content:center" title=""></button>' +
2382
- '<span id="hrToggleMsg" class="sub" style="margin:0;text-align:center;max-width:560px"></span>' +
2374
+ '<button id="sqToggle" class="btn" style="width:auto;padding:11px 16px;display:inline-flex;align-items:center;gap:8px;justify-content:center" title=""></button>' +
2375
+ '<span id="sqToggleMsg" class="sub" style="margin:0;text-align:center;max-width:560px"></span>' +
2383
2376
  '</div>' +
2384
- stHtml + statsHtml;
2377
+ stHtml + statsHtml +
2378
+ (s.calls ? '<div style="text-align:center;margin-top:12px"><button id="sqReset" class="btn" style="width:auto;padding:6px 12px;font-size:12px">İstatistiği sıfırla</button></div>' : '');
2385
2379
 
2386
- const btn = $('#hrToggle');
2380
+ const svgPlay =
2381
+ '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="6 3 20 12 6 21 6 3"/></svg>';
2382
+ const svgPause =
2383
+ '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>';
2384
+ const btn = $('#sqToggle');
2387
2385
  if (btn) {
2388
- const on = !!(st && st.enabled);
2389
2386
  /* yalnız SVG ikon — anlamı durum satırı + tooltip taşır */
2390
2387
  btn.innerHTML = on ? svgPause : svgPlay;
2391
- btn.title = on ? 'Token sıkıştırmayı kapat' : 'Token sıkıştırmayı aç';
2388
+ btn.title = on ? 'Squeeze\'i kapat' : 'Squeeze\'i aç';
2392
2389
  btn.setAttribute('aria-label', btn.title);
2393
2390
  btn.addEventListener('click', async () => {
2394
- const msg = $('#hrToggleMsg');
2391
+ const msg = $('#sqToggleMsg');
2395
2392
  btn.disabled = true;
2396
2393
  btn.innerHTML = '<span>…</span>';
2397
- const r = await beast.headroomToggle(!on).catch(() => null);
2394
+ const r = await beast.squeezeToggle(!on).catch(() => null);
2398
2395
  if (msg) {
2399
- if (r && r.installing) {
2400
- msg.textContent = 'Headroom arka planda kuruluyor — birkaç dakika sonra hazır olur, bu sekme açık kalabilir.';
2401
- } else if (r && r.error) {
2402
- msg.textContent = r.error;
2403
- } else {
2404
- msg.textContent = !on ? 'Açıldı — proxy hazır olunca trafiği yönlendirir' : 'Kapatıldı — trafik sağlayıcıya düz gidiyor';
2405
- }
2396
+ msg.textContent = r && r.ok
2397
+ ? (!on ? 'Açıldı — sonraki istekler sıkıştırılarak gönderilir' : 'Kapatıldı — istekler olduğu gibi gider')
2398
+ : 'Ayar kaydedilemedi';
2406
2399
  }
2407
- setTimeout(() => renderHeadroomPane(), 2500);
2400
+ setTimeout(() => renderSqueezePane(), 400);
2401
+ });
2402
+ }
2403
+ const rb = $('#sqReset');
2404
+ if (rb) {
2405
+ rb.addEventListener('click', async () => {
2406
+ rb.disabled = true;
2407
+ await beast.squeezeReset().catch(() => null);
2408
+ renderSqueezePane();
2408
2409
  });
2409
2410
  }
2410
2411
 
2411
2412
  /* açıkken istatistikler canlı aksın */
2412
- if (hrStatsTimer) clearInterval(hrStatsTimer);
2413
- hrStatsTimer = setInterval(async () => {
2414
- if (pane.hidden) { clearInterval(hrStatsTimer); return; }
2415
- const s2 = await beast.headroomStatus().catch(() => null);
2416
- const cur = $('#hrToggle');
2417
- if (s2 && cur && cur.disabled) return; /* toggle işlemi sürüyor */
2418
- await renderHeadroomPane();
2419
- }, 6000);
2413
+ if (sqStatsTimer) clearInterval(sqStatsTimer);
2414
+ sqStatsTimer = setInterval(() => {
2415
+ if (pane.hidden) { clearInterval(sqStatsTimer); return; }
2416
+ renderSqueezePane();
2417
+ }, 5000);
2420
2418
  }
2421
2419
 
2422
2420
  async function renderInstallPane() {
@@ -2486,24 +2484,10 @@ async function renderInstallPane() {
2486
2484
  '<div class="sub" style="font-size:11px">' + escapeHtml(r.detail || '') + (r.mb ? ' · ' + r.mb + ' MB' : '') + '</div>' +
2487
2485
  bar(r) +
2488
2486
  '</div>' +
2489
- (r.canToggle
2490
- ? '<button class="btn hr-tgl" data-on="' + (r.toggleOn ? '1' : '0') + '" style="width:auto;padding:2px 12px;flex:none">' + (r.toggleOn ? 'KAPAT' : 'AÇ') + '</button>'
2491
- : '') +
2492
2487
  badge(r.state) +
2493
2488
  pctLabel(r) +
2494
2489
  '</div>'
2495
2490
  )).join('');
2496
- /* Headroom aç/kapa: toggle kurulum + proxy başlatmayı arka planda tetikler */
2497
- el.querySelectorAll('.hr-tgl').forEach((b) => {
2498
- b.addEventListener('click', async () => {
2499
- const on = b.dataset.on !== '1';
2500
- b.disabled = true;
2501
- b.textContent = '…';
2502
- const r = await beast.headroomToggle(on).catch(() => null);
2503
- toast(r && r.error ? r.error : (r && r.installing ? 'Headroom kuruluyor — birkaç dakika sürer' : (on ? 'Headroom açıldı' : 'Headroom kapatıldı')));
2504
- refresh();
2505
- });
2506
- });
2507
2491
  };
2508
2492
 
2509
2493
  const refresh = async () => {
@@ -5745,19 +5729,6 @@ function onEvent(ev) {
5745
5729
  showPermissionCard(req, isBc);
5746
5730
  return;
5747
5731
  }
5748
- /* Headroom durum değişimi → açık olan sekmeyi tazele (Token Sıkıştırma / Kurulum) */
5749
- if (ev.type === 'headroom-status') {
5750
- try {
5751
- const iPane = document.getElementById('tab-install');
5752
- const hPane = document.getElementById('tab-headroom');
5753
- const open = (!iPane || iPane.hidden) && (!hPane || hPane.hidden) ? '' : (!hPane || hPane.hidden ? 'install' : 'headroom');
5754
- if (open) {
5755
- clearTimeout(hrPaneTimer);
5756
- hrPaneTimer = setTimeout(() => { (open === 'headroom' ? renderHeadroomPane() : renderInstallPane()).catch(() => {}); }, 1500);
5757
- }
5758
- } catch {}
5759
- return;
5760
- }
5761
5732
  /* EMPATİ LOOP: proaktif bildirim (masaüstü) + sekme canlı yenileme.
5762
5733
  Toast'ta yalnız haber başlığı — link/markdown kalabalığı chat mesajında. */
5763
5734
  if (ev.type === 'proactive') {
@@ -1,279 +0,0 @@
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
- };