beast-agent 0.28.0 → 0.29.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": "0.28.0",
4
+ "version": "0.29.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
package/src/agent/bots.js CHANGED
@@ -199,6 +199,8 @@ function add({ name, icon, prompt }) {
199
199
  loadRegistry();
200
200
  const n = String(name || '').trim().slice(0, 40);
201
201
  if (!n) return { ok: false, error: 'bot adı zorunlu' };
202
+ /* İLK HARF ZORUNLU: bot adı harf karakteriyle başlamalı */
203
+ if (!/^\p{L}/u.test(n)) return { ok: false, error: 'bot adı harf ile başlamalı — ilk karakter harf olmalı' };
202
204
  if (REG.bots.length >= MAX_BOTS) return { ok: false, error: `en fazla ${MAX_BOTS} bot olabilir (1 admin + ${MAX_BOTS - 1} müşteri)` };
203
205
  let id = slugify(n);
204
206
  while (get(id)) id = slugify(n) + '-' + uid().slice(-4);
@@ -261,6 +263,12 @@ function update(id, patch) {
261
263
  loadRegistry();
262
264
  const b = get(id);
263
265
  if (!b) return { ok: false, error: 'bot yok' };
266
+ /* İLK HARF ZORUNLU: ad değişikliği harf karakteriyle başlamalı —
267
+ diğer alanlara dokunmadan reddet */
268
+ if (patch && typeof patch.name === 'string' && patch.name.trim() &&
269
+ !/^\p{L}/u.test(patch.name.trim())) {
270
+ return { ok: false, error: 'bot adı harf ile başlamalı — ilk karakter harf olmalı' };
271
+ }
264
272
  const changes = [];
265
273
  if (patch && typeof patch === 'object') {
266
274
  if (typeof patch.name === 'string' && patch.name.trim() && patch.name.trim() !== b.name) {
@@ -630,10 +630,13 @@ class Engine {
630
630
  {
631
631
  signal,
632
632
  onDelta,
633
- onRetry: (attempt) =>
633
+ onRetry: (attempt, st) =>
634
634
  emitSafe(this, session.id, {
635
635
  type: 'status',
636
- status: `sağlayıcı yanıtsız — tekrar deniyor (${attempt}/${3})`,
636
+ status:
637
+ st === undefined || st === null
638
+ ? `bağlantı sorunu — internet dönünce ya da yeniden denemeyle sürer (${attempt}/6)`
639
+ : `sağlayıcı yanıtsız — tekrar deniyor (${attempt}/3)`,
637
640
  }),
638
641
  }
639
642
  );
package/src/agent/llm.js CHANGED
@@ -1,10 +1,51 @@
1
1
  'use strict';
2
2
 
3
3
  /* OpenAI-compatible streaming chat client with SSE parsing.
4
- Geçici sağlayıcı hatalarında (5xx/429/ağ) üstel beklemeyle otomatik retry. */
4
+ Geçici sağlayıcı hatalarında (5xx/429) üstel beklemeyle retry; İNTERNET
5
+ KOPMASINDA (fetch failed) çok daha sabırlı: 6 deneme + internet dönene
6
+ dek bekleme (main'deki net izleyicisinden beslenir). Akış ortasında kopan
7
+ bağlantıda: hiç veri akmadıysa sessiz baştan dener, kısmi metin geldiyse
8
+ 'length' gibi döner → chatStreamAuto kaldığı yerden DEVAM ETTİRİR. */
5
9
 
6
10
  const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
7
- const MAX_RETRIES = 3;
11
+ const MAX_RETRIES = 3; /* sağlayıcı geçici HTTP hataları */
12
+ const NET_MAX_RETRIES = 6; /* bağlantı kopması — daha sabırlı */
13
+ const NET_BACKOFF = [1000, 2000, 4000, 8000, 15000, 25000]; /* ≈55 sn toplam */
14
+ const NET_WAIT_CAP_MS = 120000; /* internet dönene dek bekleme üst sınırı */
15
+
16
+ /* main process'teki net izleyicisi besler: () => boolean | Promise<boolean> */
17
+ let _netProbe = null;
18
+ function setNetProbe(fn) {
19
+ _netProbe = typeof fn === 'function' ? fn : null;
20
+ }
21
+
22
+ /* fetch failed / ENOTFOUND / ECONNRESET / socket hang up / terminated —
23
+ HTTP yanıtı ALINAMADI demektir (status yok) */
24
+ function isNetworkError(e) {
25
+ if (!e) return false;
26
+ if (e.name === 'AbortError') return false;
27
+ if (e.status !== undefined) return false;
28
+ return true;
29
+ }
30
+
31
+ /* internet yoksa dönene kadar bekle (en fazla NET_WAIT_CAP_MS); probe yoksa
32
+ ya da durum bilinmiyorsa hemen geç */
33
+ async function waitForNet(signal) {
34
+ if (!_netProbe) return true;
35
+ const t0 = Date.now();
36
+ for (;;) {
37
+ if (signal && signal.aborted) {
38
+ const e = new Error('iptal');
39
+ e.name = 'AbortError';
40
+ throw e;
41
+ }
42
+ let online;
43
+ try { online = await _netProbe(); } catch { online = null; }
44
+ if (online !== false) return true; /* false DEĞİLSE devam (bilinmiyor = iyimser) */
45
+ if (Date.now() - t0 > NET_WAIT_CAP_MS) return false;
46
+ await sleep(2000, signal);
47
+ }
48
+ }
8
49
 
9
50
  const HINTS = {
10
51
  401: 'API anahtarı geçersiz',
@@ -47,8 +88,9 @@ function sleep(ms, signal) {
47
88
  });
48
89
  }
49
90
 
50
- /* fn: denenecek istek. hataları ve RETRYABLE_STATUS durumlarında
51
- 0.8s 1.6s 3.2s bekleyip tekrar dener. */
91
+ /* fn: denenecek istek. Sağlayıcı geçici HTTP hatalarında 0.8s→1.6s→3.2s;
92
+ İNTERNET KOPMASINDA (status yok) 6 denemeye kadar 1s→2s→4s→8s→15s→25s ve
93
+ net izleyicisi "çevrimdışı" diyorsa internet dönene kadar bekler. */
52
94
  async function withRetries(fn, { signal, onRetry } = {}) {
53
95
  let attempt = 0;
54
96
  for (;;) {
@@ -57,14 +99,23 @@ async function withRetries(fn, { signal, onRetry } = {}) {
57
99
  } catch (e) {
58
100
  const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
59
101
  const status = e && e.status;
60
- const retriable =
61
- !aborted && (status === undefined || RETRYABLE_STATUS.has(status));
62
- if (!retriable || attempt >= MAX_RETRIES) throw e;
102
+ const netErr = !aborted && status === undefined;
103
+ const retriable = !aborted && (status === undefined || RETRYABLE_STATUS.has(status));
104
+ if (!retriable) throw e;
105
+ const cap = netErr ? NET_MAX_RETRIES : MAX_RETRIES;
106
+ if (attempt >= cap) throw e;
107
+ if (netErr && _netProbe) {
108
+ /* internet kopmuş: dönene kadar bekle — kısa kopmalarda görev ölmez */
109
+ await waitForNet(signal);
110
+ }
63
111
  attempt++;
64
112
  try {
65
113
  onRetry && onRetry(attempt, status);
66
114
  } catch {}
67
- await sleep(800 * Math.pow(2, attempt - 1), signal);
115
+ const wait = netErr
116
+ ? NET_BACKOFF[Math.min(attempt - 1, NET_BACKOFF.length - 1)]
117
+ : 800 * Math.pow(2, attempt - 1);
118
+ await sleep(wait, signal);
68
119
  }
69
120
  }
70
121
  }
@@ -113,79 +164,101 @@ async function chatStream(sel, body, { signal, onDelta, onRetry } = {}) {
113
164
  }
114
165
 
115
166
  async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReasoning = false) {
116
- const res = await withRetries(
117
- async () => {
118
- const r = await openChat(sel, body, { stream: true, signal, omitReasoning });
119
- if (!r.ok) {
120
- let detail = '';
121
- try {
122
- detail = (await r.text()).slice(0, 300);
123
- } catch {}
124
- const err = new Error(friendlyError(r.status, r.statusText, detail));
125
- err.status = r.status;
126
- throw err;
127
- }
128
- return r;
129
- },
130
- { signal, onRetry }
131
- );
167
+ /* akış durumu dışarıda tutulur: gövde okunurken bağlantı koparsa
168
+ ne kadarı geldiğine bakılır (kısmi metin → devam; boş/araç → baştan) */
169
+ const state = { content: '', reasoning: '', toolCalls: [], usage: null, finishReason: null };
170
+ let dropAttempt = 0;
171
+
172
+ for (;;) {
173
+ const res = await withRetries(
174
+ async () => {
175
+ const r = await openChat(sel, body, { stream: true, signal, omitReasoning });
176
+ if (!r.ok) {
177
+ let detail = '';
178
+ try {
179
+ detail = (await r.text()).slice(0, 300);
180
+ } catch {}
181
+ const err = new Error(friendlyError(r.status, r.statusText, detail));
182
+ err.status = r.status;
183
+ throw err;
184
+ }
185
+ return r;
186
+ },
187
+ { signal, onRetry }
188
+ );
132
189
 
133
- let content = '';
134
- let reasoning = '';
135
- const toolCalls = [];
136
- let usage = null;
137
- let finishReason = null;
190
+ try {
191
+ const reader = res.body.getReader();
192
+ const decoder = new TextDecoder();
193
+ let buf = '';
138
194
 
139
- const reader = res.body.getReader();
140
- const decoder = new TextDecoder();
141
- let buf = '';
195
+ while (true) {
196
+ const { done, value } = await reader.read();
197
+ if (done) break;
198
+ buf += decoder.decode(value, { stream: true });
142
199
 
143
- while (true) {
144
- const { done, value } = await reader.read();
145
- if (done) break;
146
- buf += decoder.decode(value, { stream: true });
200
+ let idx;
201
+ while ((idx = buf.indexOf('\n')) !== -1) {
202
+ const line = buf.slice(0, idx).trim();
203
+ buf = buf.slice(idx + 1);
204
+ if (!line.startsWith('data:')) continue;
205
+ const data = line.slice(5).trim();
206
+ if (!data || data === '[DONE]') continue;
147
207
 
148
- let idx;
149
- while ((idx = buf.indexOf('\n')) !== -1) {
150
- const line = buf.slice(0, idx).trim();
151
- buf = buf.slice(idx + 1);
152
- if (!line.startsWith('data:')) continue;
153
- const data = line.slice(5).trim();
154
- if (!data || data === '[DONE]') continue;
208
+ let json;
209
+ try {
210
+ json = JSON.parse(data);
211
+ } catch {
212
+ continue;
213
+ }
155
214
 
156
- let json;
157
- try {
158
- json = JSON.parse(data);
159
- } catch {
160
- continue;
215
+ if (json.usage) state.usage = json.usage;
216
+ const ch = json.choices && json.choices[0];
217
+ if (!ch) continue;
218
+ if (ch.finish_reason) state.finishReason = ch.finish_reason;
219
+ const d = ch.delta || {};
220
+ if (d.content) {
221
+ state.content += d.content;
222
+ onDelta && onDelta(d.content, state.content);
223
+ }
224
+ if (d.reasoning_content) state.reasoning += d.reasoning_content;
225
+ if (d.reasoning) state.reasoning += d.reasoning;
226
+ for (const tc of d.tool_calls || []) {
227
+ const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
228
+ while (state.toolCalls.length <= i) {
229
+ state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
230
+ }
231
+ if (tc.id) state.toolCalls[i].id = tc.id;
232
+ if (tc.function) {
233
+ if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
234
+ if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
235
+ }
236
+ }
237
+ }
161
238
  }
162
239
 
163
- if (json.usage) usage = json.usage;
164
- const ch = json.choices && json.choices[0];
165
- if (!ch) continue;
166
- if (ch.finish_reason) finishReason = ch.finish_reason;
167
- const d = ch.delta || {};
168
- if (d.content) {
169
- content += d.content;
170
- onDelta && onDelta(d.content, content);
171
- }
172
- if (d.reasoning_content) reasoning += d.reasoning_content;
173
- if (d.reasoning) reasoning += d.reasoning;
174
- for (const tc of d.tool_calls || []) {
175
- const i = typeof tc.index === 'number' ? tc.index : toolCalls.length;
176
- while (toolCalls.length <= i) {
177
- toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
178
- }
179
- if (tc.id) toolCalls[i].id = tc.id;
180
- if (tc.function) {
181
- if (tc.function.name) toolCalls[i].function.name += tc.function.name;
182
- if (tc.function.arguments) toolCalls[i].function.arguments += tc.function.arguments;
240
+ return { ...state };
241
+ } catch (e) {
242
+ /* okuma sırasında bağlantı koptu */
243
+ const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
244
+ if (!aborted && isNetworkError(e)) {
245
+ if (state.content && !state.toolCalls.length) {
246
+ /* kısmi metin geldi, araç çağrısı yok → hata balonu YOK:
247
+ 'length' gibi dön, chatStreamAuto CONTINUE_PROMPT ile kaldığı
248
+ yerden sürdürür ve metni birleştirir */
249
+ return { ...state, finishReason: 'length' };
183
250
  }
251
+ /* hiç veri akmadı YA DA araç çağrısı yarım kaldı → BAŞTAN dene */
252
+ if (dropAttempt >= NET_MAX_RETRIES) throw e;
253
+ dropAttempt++;
254
+ try { onRetry && onRetry(dropAttempt, undefined); } catch {}
255
+ if (_netProbe) await waitForNet(signal);
256
+ await sleep(NET_BACKOFF[Math.min(dropAttempt - 1, NET_BACKOFF.length - 1)], signal);
257
+ continue; /* yeni stream — UI'daki kısmi token'lar nihai mesajla ezilir */
184
258
  }
259
+ throw e;
185
260
  }
186
261
  }
187
-
188
- return { content, reasoning, toolCalls, usage, finishReason };
189
262
  }
190
263
 
191
264
  /* usage toplama: devam turları gerçek faturalamayı yansıtsın (prompt yeniden sayılır) */
@@ -261,4 +334,13 @@ async function chatOnce(sel, body, { signal, onRetry } = {}) {
261
334
  };
262
335
  }
263
336
 
264
- module.exports = { chatStream, chatStreamAuto, chatOnce, withRetries, friendlyError, sumUsage };
337
+ module.exports = {
338
+ chatStream,
339
+ chatStreamAuto,
340
+ chatOnce,
341
+ withRetries,
342
+ friendlyError,
343
+ sumUsage,
344
+ setNetProbe,
345
+ isNetworkError,
346
+ };
@@ -20,6 +20,8 @@ const RELEASE_BASE = 'https://github.com/h4ckf0r0day/obscura/releases/latest/dow
20
20
  const WINDOWS_ZIP = 'obscura-x86_64-windows-stealth.zip';
21
21
  const MAX_ZIP_BYTES = 250 * 1024 * 1024;
22
22
  const FETCH_TIMEOUT_MS = 25000;
23
+ /* content-length gelmezse yüzde tahmini için yaklaşık boyut (stealth zip ≈74 MB) */
24
+ const EST_ZIP_BYTES = 74 * 1024 * 1024;
23
25
 
24
26
  function obscuraDir() {
25
27
  const base = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
@@ -58,19 +60,29 @@ function obscuraOk() {
58
60
  return _okProbe;
59
61
  }
60
62
 
61
- function httpsDownload(url, redirectsLeft = 5, signal) {
63
+ function httpsDownload(url, redirectsLeft = 5, signal, onProgress) {
62
64
  return new Promise((resolve, reject) => {
63
65
  const req = https.get(url, { headers: { 'User-Agent': 'BeastAgent/1.0 (+obscura bootstrap)' } }, (res) => {
64
66
  if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirectsLeft > 0) {
65
67
  res.resume();
66
- return resolve(httpsDownload(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal));
68
+ return resolve(httpsDownload(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal, onProgress));
67
69
  }
68
70
  if (res.statusCode !== 200) {
69
71
  res.resume();
70
72
  return reject(new Error(`indirme başarısız: HTTP ${res.statusCode}`));
71
73
  }
74
+ const total = Number(res.headers['content-length']) || EST_ZIP_BYTES;
72
75
  const chunks = [];
73
76
  let size = 0;
77
+ let lastPct = -1;
78
+ const tick = () => {
79
+ /* yüzdeyi INDIRME fazının %1-88 aralığına map et; %1'lik adımda bildir */
80
+ const pct = 1 + Math.min(88, Math.floor((size / total) * 88));
81
+ if (typeof onProgress === 'function' && pct !== lastPct) {
82
+ lastPct = pct;
83
+ try { onProgress({ pct, phase: 'indiriliyor', loaded: size, total }); } catch {}
84
+ }
85
+ };
74
86
  res.on('data', (c) => {
75
87
  size += c.length;
76
88
  if (size > MAX_ZIP_BYTES) {
@@ -78,8 +90,12 @@ function httpsDownload(url, redirectsLeft = 5, signal) {
78
90
  return;
79
91
  }
80
92
  chunks.push(c);
93
+ tick();
94
+ });
95
+ res.on('end', () => {
96
+ tick();
97
+ resolve(Buffer.concat(chunks));
81
98
  });
82
- res.on('end', () => resolve(Buffer.concat(chunks)));
83
99
  res.on('error', reject);
84
100
  });
85
101
  req.on('error', reject);
@@ -114,22 +130,37 @@ function psExpand(zipPath, destDir) {
114
130
  });
115
131
  }
116
132
 
117
- /* Obscura'yı kur / güncelle (yeniden indirip üzerine yazar) */
118
- async function installObscura(signal) {
119
- const dest = obscuraDir();
120
- fs.mkdirSync(dest, { recursive: true });
121
- const zipPath = path.join(os.tmpdir(), 'beast-obscura.zip');
122
- const buf = await httpsDownload(RELEASE_BASE + WINDOWS_ZIP, 5, signal);
123
- fs.writeFileSync(zipPath, buf);
124
- const ok = await psExpand(zipPath, dest);
125
- try { fs.unlinkSync(zipPath); } catch {}
126
- if (!ok || !obscuraInstalled()) {
127
- return { ok: false, error: 'obscura kurulumu başarısız (zip açılamadı)' };
133
+ /* Obscura'yı kur / güncelle (yeniden indirip üzerine yazar).
134
+ onProgress({pct, phase}) — 1-88 indirme, 89-96 açma, 97-99 doğrulama. */
135
+ async function installObscura(signal, onProgress) {
136
+ const tick = (pct, phase) => {
137
+ if (typeof onProgress === 'function') {
138
+ try { onProgress({ pct, phase: phase || 'indiriliyor' }); } catch {}
139
+ }
140
+ };
141
+ try {
142
+ tick(0, 'hazırlanıyor');
143
+ const dest = obscuraDir();
144
+ fs.mkdirSync(dest, { recursive: true });
145
+ const zipPath = path.join(os.tmpdir(), 'beast-obscura.zip');
146
+ const buf = await httpsDownload(RELEASE_BASE + WINDOWS_ZIP, 5, signal, onProgress);
147
+ tick(89, 'kuruluyor');
148
+ fs.writeFileSync(zipPath, buf);
149
+ tick(90, 'kuruluyor');
150
+ const ok = await psExpand(zipPath, dest);
151
+ try { fs.unlinkSync(zipPath); } catch {}
152
+ tick(97, 'doğrulanıyor');
153
+ if (!ok || !obscuraInstalled()) {
154
+ return { ok: false, error: 'obscura kurulumu başarısız (zip açılamadı)' };
155
+ }
156
+ markVariant('stealth');
157
+ _okProbe = null; /* yeniden doğrula */
158
+ const good = await obscuraOk();
159
+ tick(good ? 100 : 98, good ? 'tamamlandı' : 'doğrulanıyor');
160
+ return { ok: good, dir: dest, error: good ? null : 'obscura.exe doğrulanamadı' };
161
+ } catch (e) {
162
+ return { ok: false, error: String((e && e.message) || e) };
128
163
  }
129
- markVariant('stealth');
130
- _okProbe = null; /* yeniden doğrula */
131
- const good = await obscuraOk();
132
- return { ok: good, dir: dest, error: good ? null : 'obscura.exe doğrulanamadı' };
133
164
  }
134
165
 
135
166
  /* obscura fetch — sayfa HTML'i (abort ile süreç kesilir) */
package/src/main.js CHANGED
@@ -224,13 +224,10 @@ startHealthServer(); /* splash/boot aşamasından itibaren /health ayakta */
224
224
  try { setSearchObscuraEnabled(settings.obscuraEnabled !== false); } catch {} /* Obscura varsayılan AKTİF */
225
225
  try { setSearchChain(settings.searchChain); } catch {}
226
226
  try { setTinyfishKey(settings.tinyfishKey || null); } catch {}
227
- /* kurulumda Obscura da kurulsun: yoksa ARKA PLANDA otomatik indir (UI kilitlenmez) */
227
+ /* kurulumda Obscura da kurulsun: yoksa ARKA PLANDA otomatik indir (UI kilitlenmez);
228
+ ilerleme Ayarlar → Web Arama'dan da izlenebilir */
228
229
  setTimeout(() => {
229
- if (!obscura.obscuraInstalled()) {
230
- obscura.installObscura().then((r) => {
231
- console.log('[obscura]', r.ok ? 'kuruldu: ' + r.dir : 'kurulamadı: ' + (r.error || '?'));
232
- }).catch((e) => console.log('[obscura] kurulamadı:', String((e && e.message) || e)));
233
- }
230
+ if (!obscura.obscuraInstalled()) startObscuraInstall();
234
231
  }, 4000).unref?.();
235
232
  let wa = null;
236
233
  let waChats = new Map(); // jid -> aktif session id
@@ -634,10 +631,24 @@ function emitWaEventSafe(ev) {
634
631
 
635
632
 
636
633
 
634
+ /* sürüm: app.getVersion() + package.json yedeği (splash, /help, /version ortak) */
635
+ function beastVersion() {
636
+ try {
637
+ const v = app.getVersion();
638
+ if (v && v !== '0.0.0') return v;
639
+ } catch {}
640
+ try {
641
+ return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version || '?';
642
+ } catch {
643
+ return '?';
644
+ }
645
+ }
646
+
637
647
  function waSlashHelp() {
638
648
  return [
639
649
  '*Beast komutları*',
640
650
  '• */help* – bu liste',
651
+ '• */version* – Beast Agent sürümünü göster',
641
652
  '• */new* – yeni oturum aç (kod verilir)',
642
653
  '• */open* <kod> – o koddaki oturuma geç',
643
654
  '• */sessions* – bu sohbetin oturumları',
@@ -1135,6 +1146,8 @@ async function tryWaSlash(jid, rawText, senderNum) {
1135
1146
  out =
1136
1147
  `*WA:* ${wst.status}${wst.user ? ' (' + wst.user + ')' : ''}\n` +
1137
1148
  `*İzleyici:* ${watchers.list().length} adet\n*Cron:* ${jobs} aktif görev`;
1149
+ } else if (cmd === 'version') {
1150
+ out = `*Beast Agent v${beastVersion()}*\nGüncelleme için: /update (kurulum hazır olunca /update now)`;
1138
1151
  } else {
1139
1152
  out = `Bilinmeyen komut: /${cmd}\nListe için /help yaz.`;
1140
1153
  }
@@ -3673,7 +3686,7 @@ function createSplash() {
3673
3686
  const muted = dark ? '#9a9aa2' : '#707078';
3674
3687
  splash = new BrowserWindow({
3675
3688
  width: 420,
3676
- height: 330,
3689
+ height: 352,
3677
3690
  frame: false,
3678
3691
  resizable: false,
3679
3692
  alwaysOnTop: true,
@@ -3686,6 +3699,7 @@ function createSplash() {
3686
3699
  body{margin:0;height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;background:${bg};font-family:'Segoe UI',sans-serif;color:${fg}}
3687
3700
  .logo{width:84px;height:84px;border-radius:20px;background:${fg};color:${bg};display:flex;align-items:center;justify-content:center;font-weight:900;font-size:44px}
3688
3701
  .t{margin-top:16px;font-size:20px;color:${fg}}.t b{font-weight:900}
3702
+ .v{margin-top:8px;font-size:12.5px;font-weight:800;letter-spacing:.6px;color:${muted};background:${muted}22;padding:2px 12px;border-radius:9px}
3689
3703
  .s{margin-top:6px;font-size:12px;color:${muted}}
3690
3704
  .cmds{margin-top:14px;display:flex;flex-wrap:wrap;gap:6px;justify-content:center;max-width:360px}
3691
3705
  .cmds b{font-size:11.5px;font-weight:800;color:${fg};background:${muted}22;padding:2px 9px;border-radius:6px;letter-spacing:.3px}
@@ -3696,9 +3710,10 @@ function createSplash() {
3696
3710
  </style></head><body>
3697
3711
  <div class="logo">B</div>
3698
3712
  <div class="t"><b>BEAST</b> Agent</div>
3699
- <div class="s">hızlı · hafif · becerikli — v${app.getVersion()}</div>
3713
+ <div class="v">v${beastVersion()}</div>
3714
+ <div class="s">hızlı · hafif · becerikli</div>
3700
3715
  <div class="cmds">
3701
- <b>/help</b><b>/restart</b><b>/change</b><b>/think</b><b>/clear</b><b>/stop</b><b>/usage</b><b>/backup</b><b>/status</b><span>…</span>
3716
+ <b>/help</b><b>/version</b><b>/restart</b><b>/change</b><b>/think</b><b>/clear</b><b>/stop</b><b>/usage</b><b>/backup</b><b>/status</b><span>…</span>
3702
3717
  </div>
3703
3718
  <div class="bar"><i></i></div>
3704
3719
  </body></html>`;
@@ -3830,6 +3845,10 @@ ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
3830
3845
  handleRestart(sessionId);
3831
3846
  return true;
3832
3847
  }
3848
+ if (t === '/version') {
3849
+ desktopEcho(sessionId, t, `**Beast Agent v${beastVersion()}**\nGüncelleme: **/update** (yeni sürüm kontrolü) · **/update now** (hemen kur)`);
3850
+ return true;
3851
+ }
3833
3852
  if (t === '/help') {
3834
3853
  desktopEcho(sessionId, '/help', desktopSlashHelp());
3835
3854
  return true;
@@ -3979,6 +3998,7 @@ function desktopSlashHelp() {
3979
3998
  return [
3980
3999
  '**Beast komutları**',
3981
4000
  '**/help** – bu liste',
4001
+ '**/version** – Beast Agent sürümünü göster',
3982
4002
  '**/restart** – uygulamayı yeniden başlat',
3983
4003
  '**/stop** – koşan işleri durdur · **/start** – devam ettir',
3984
4004
  '**/change [n]** – modelleri listele · n. modele geç',
@@ -4081,6 +4101,13 @@ function thinkStatusText() {
4081
4101
  }
4082
4102
 
4083
4103
  /* /restart: uygulama kendini yeniden başlatır (relaunch + exit) */
4104
+ function scheduleAppRestart(delayMs = 800) {
4105
+ setTimeout(() => {
4106
+ try { app.relaunch(); } catch {}
4107
+ try { app.exit(0); } catch {}
4108
+ }, delayMs);
4109
+ }
4110
+
4084
4111
  function handleRestart(sessionId) {
4085
4112
  const sid = String(sessionId || ''); if (win && !win.isDestroyed()) {
4086
4113
  win.webContents.send('agent:event', { sessionId: sid, type: 'message', message: { role: 'user', content: '/restart' } });
@@ -4091,10 +4118,7 @@ function handleRestart(sessionId) {
4091
4118
  });
4092
4119
  win.webContents.send('agent:event', { sessionId: sid, type: 'done', usage: null });
4093
4120
  }
4094
- setTimeout(() => {
4095
- try { app.relaunch(); } catch {}
4096
- try { app.exit(0); } catch {}
4097
- }, 800);
4121
+ scheduleAppRestart(800);
4098
4122
  }
4099
4123
 
4100
4124
  /* Masaüstünde /stop ve /start: engine'e gitmez; tüm sistemde etki eder ve
@@ -4241,6 +4265,10 @@ let netFailStreak = 0;
4241
4265
  let chatQueueFlushing = false;
4242
4266
  const chatOfflineQueue = []; // { key, sessionId, text, attachments, at }
4243
4267
 
4268
+ /* LLM retry'ı net izleyicisinden besler: internet kopmuşsa istek,
4269
+ bağlantı dönene kadar bekler — "fetch failed" ile görev ölmez */
4270
+ try { require('./agent/llm').setNetProbe(() => netOnline); } catch {}
4271
+
4244
4272
  /* diskten yükle (app restart sonrası kuyruk korunur) */
4245
4273
  (function chatQueueLoad() {
4246
4274
  try {
@@ -5225,19 +5253,50 @@ ipcMain.handle('think:set', (_e, v) => {
5225
5253
  });
5226
5254
 
5227
5255
  /* Obscura stealth headless tarayıcı (Ayarlar → Web Arama) */
5256
+
5257
+ /* kurulum durumu: ayarlardan çıkılıp dönülsa da main process'te sürer;
5258
+ ilerleme agent:event ile panele, obscura:installState ile sekme açılışına taşınır */
5259
+ let obscuraInstallState = { running: false, pct: 0, phase: '', error: null };
5260
+
5261
+ function pushObscuraProgress() {
5262
+ try {
5263
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'obscura-progress', ...obscuraInstallState });
5264
+ } catch {}
5265
+ }
5266
+
5267
+ function startObscuraInstall() {
5268
+ if (obscuraInstallState.running) return { ok: false, busy: true, ...obscuraInstallState };
5269
+ obscuraInstallState = { running: true, pct: 0, phase: 'hazırlanıyor', error: null };
5270
+ pushObscuraProgress();
5271
+ obscura
5272
+ .installObscura(null, (p) => {
5273
+ obscuraInstallState.pct = Math.max(0, Math.min(100, Math.round(Number(p && p.pct) || 0)));
5274
+ obscuraInstallState.phase = String((p && p.phase) || obscuraInstallState.phase || '');
5275
+ pushObscuraProgress();
5276
+ })
5277
+ .then((r) => {
5278
+ obscuraInstallState = r && r.ok
5279
+ ? { running: false, pct: 100, phase: 'tamamlandı', error: null }
5280
+ : { running: false, pct: 0, phase: 'hata', error: String((r && r.error) || 'bilinmeyen hata') };
5281
+ pushObscuraProgress();
5282
+ console.log('[obscura]', r && r.ok ? 'kuruldu: ' + r.dir : 'kurulamadı: ' + ((r && r.error) || '?'));
5283
+ })
5284
+ .catch((e) => {
5285
+ obscuraInstallState = { running: false, pct: 0, phase: 'hata', error: String((e && e.message) || e) };
5286
+ pushObscuraProgress();
5287
+ console.log('[obscura] kurulamadı:', String((e && e.message) || e));
5288
+ });
5289
+ return { ok: true, started: true, ...obscuraInstallState };
5290
+ }
5291
+
5228
5292
  ipcMain.handle('obscura:get', () => ({
5229
5293
  installed: obscura.obscuraInstalled(),
5230
5294
  dir: obscura.obscuraDir(),
5231
5295
  enabled: settings.obscuraEnabled !== false,
5296
+ install: { ...obscuraInstallState },
5232
5297
  }));
5233
- ipcMain.handle('obscura:install', async () => {
5234
- try {
5235
- const r = await obscura.installObscura();
5236
- return { ok: !!r.ok, dir: r.dir || obscura.obscuraDir(), error: r.error || null };
5237
- } catch (e) {
5238
- return { ok: false, error: String((e && e.message) || e) };
5239
- }
5240
- });
5298
+ ipcMain.handle('obscura:install', () => startObscuraInstall());
5299
+ ipcMain.handle('obscura:installState', () => ({ ...obscuraInstallState }));
5241
5300
  ipcMain.handle('obscura:setEnabled', (_e, v) => {
5242
5301
  settings.obscuraEnabled = v !== false;
5243
5302
  saveSettings();
@@ -6236,8 +6295,11 @@ ipcMain.handle('bots:remove', (_e, id) => {
6236
6295
  saveSettings();
6237
6296
  syncWhitelist();
6238
6297
  log.info('main', `bot silindi: ${id} — bağlı numaralar botsuz (beast'e düşer)`);
6298
+ /* bot silme sonrası sistem KİTLENİYOR (oturum/DM/servis referansları) →
6299
+ güvenli yol: bot kaydı silindikten sonra uygulamayı temiz yeniden başlat */
6300
+ if (String(id || '') !== 'beast') scheduleAppRestart(1200);
6239
6301
  }
6240
- return { ...r, list: r.ok ? botListWithNumbers() : null };
6302
+ return { ...r, list: r.ok ? botListWithNumbers() : null, restarting: r.ok && String(id || '') !== 'beast' };
6241
6303
  });
6242
6304
 
6243
6305
  ipcMain.handle('bots:stats', () => botStats());
package/src/preload.js CHANGED
@@ -72,6 +72,7 @@ contextBridge.exposeInMainWorld('beast', {
72
72
  thinkSet: (v) => ipcRenderer.invoke('think:set', v),
73
73
  obscuraGet: () => ipcRenderer.invoke('obscura:get'),
74
74
  obscuraInstall: () => ipcRenderer.invoke('obscura:install'),
75
+ obscuraInstallState: () => ipcRenderer.invoke('obscura:installState'),
75
76
  obscuraSetEnabled: (v) => ipcRenderer.invoke('obscura:setEnabled', v),
76
77
  searchOrderGet: () => ipcRenderer.invoke('searchorder:get'),
77
78
  searchOrderSet: (chain) => ipcRenderer.invoke('searchorder:set', chain),
@@ -18,6 +18,9 @@
18
18
  tipThink: 'Düşünme (reasoning) seviyesi',
19
19
  tipPickCfg: 'Picker\u2019da görünecek modeller',
20
20
  tipRail: 'Paralel Ajan Konsolu',
21
+ tipNet: 'İnternet durumu',
22
+ tip_net_online: 'İnternet: bağlı',
23
+ tip_net_offline: 'İnternet: yok — bağlantı dönünce işler otomatik sürer',
21
24
  tipWatch: 'İzleyiciler (Watchers)',
22
25
  tipCron: 'Cron Görevler',
23
26
  tip_light: 'Açık tema',
@@ -266,7 +269,14 @@
266
269
  oc_status_ok: 'Obscura kurulu ve hazır.',
267
270
  oc_status_missing: 'Obscura kurulu değil — ilk açılışta otomatik indirilir (≈74 MB).',
268
271
  oc_install: 'Kur / Güncelle',
272
+ oc_install_running: 'Kuruluyor…',
269
273
  oc_installing: 'İndiriliyor… (≈74 MB, bağlantıya göre birkaç dakika)',
274
+ oc_phase_prep: 'Hazırlanıyor',
275
+ oc_phase_download: 'İndiriliyor',
276
+ oc_phase_extract: 'Açılıyor',
277
+ oc_phase_verify: 'Doğrulanıyor',
278
+ oc_phase_done: 'Tamamlandı',
279
+ oc_bg_note: 'Kurulum arka planda sürüyor — ayarlardan çıkıp dönsen bile ilerleme burada devam eder.',
270
280
  oc_installed_toast: 'Obscura kuruldu',
271
281
  oc_install_fail: 'Kurulamadı: ',
272
282
  oc_enabled: 'Obscura arama zincirinde aktif',
@@ -445,8 +455,9 @@
445
455
  bot_delete: 'Botu Sil',
446
456
  bot_confirm_del: '"${n}" silinsin mi? Bağlı numaralar botsuz duruma düşer (Beast\u2019e yönlendirilir).',
447
457
  bot_deleted: 'Bot silindi',
458
+ bot_deleted_restart: 'Bot silindi — sistem temiz yeniden başlatılıyor…',
448
459
  bot_max_toast: 'En fazla ${n} bot olabilir (1 admin + 4 müşteri)',
449
- bot_name_ph: 'Bot adı (örn: Muhasebe Botu)',
460
+ bot_name_ph: 'Bot adı (harfle başlar, örn: Muhasebe Botu)',
450
461
  bot_icon_ph: 'İkon seç (emoji kopyala-yapıştır)',
451
462
  bot_created: 'Bot oluşturuldu:',
452
463
  bot_mem_sub: 'Bu botun KENDİ hafızası — SOUL (kişilik) · USER (kullanıcı bilgisi) · MEMORY (hafıza kayıtları). Diğer botlardan tamamen izoledir.',
@@ -476,6 +487,7 @@
476
487
  bot_create: 'Botu Oluştur',
477
488
  bot_cancel: 'Vazgeç',
478
489
  bot_name_req: 'Bot adı zorunlu',
490
+ bot_name_letter: 'Bot adı HARF ile başlamalı — ilk karakter harf olmalı',
479
491
  p_preset: 'Hazır sağlayıcı (endpoint otomatik)',
480
492
  p_preset_custom: '— elle gir —',
481
493
  p_preset_hint: 'Hazır seçersen adres otomatik dolar; sadece API key girip modelleri çek.',
@@ -560,6 +572,9 @@
560
572
  tipThink: 'Reasoning level',
561
573
  tipPickCfg: 'Models shown in picker',
562
574
  tipRail: 'Parallel Agents Console',
575
+ tipNet: 'Internet status',
576
+ tip_net_online: 'Internet: connected',
577
+ tip_net_offline: 'Internet: offline — work resumes automatically once the connection returns',
563
578
  tipWatch: 'Watchers',
564
579
  tipCron: 'Cron Jobs',
565
580
  tip_light: 'Light theme',
@@ -807,7 +822,14 @@
807
822
  oc_status_ok: 'Obscura is installed and ready.',
808
823
  oc_status_missing: 'Obscura is not installed — it is auto-downloaded at first launch (≈74 MB).',
809
824
  oc_install: 'Install / Update',
825
+ oc_install_running: 'Installing…',
810
826
  oc_installing: 'Downloading… (≈74 MB, may take a few minutes)',
827
+ oc_phase_prep: 'Preparing',
828
+ oc_phase_download: 'Downloading',
829
+ oc_phase_extract: 'Extracting',
830
+ oc_phase_verify: 'Verifying',
831
+ oc_phase_done: 'Done',
832
+ oc_bg_note: 'Installation continues in the background — progress stays here even if you leave the settings.',
811
833
  oc_installed_toast: 'Obscura installed',
812
834
  oc_install_fail: 'Install failed: ',
813
835
  oc_enabled: 'Obscura active in the search chain',
@@ -1044,8 +1066,9 @@
1044
1066
  bot_delete: 'Delete Bot',
1045
1067
  bot_confirm_del: 'Delete "${n}"? Linked numbers become botless (routed to Beast).',
1046
1068
  bot_deleted: 'Bot deleted',
1069
+ bot_deleted_restart: 'Bot deleted — system is restarting cleanly…',
1047
1070
  bot_max_toast: 'Maximum ${n} bots (1 admin + 4 customer)',
1048
- bot_name_ph: 'Bot name (e.g. Accounting Bot)',
1071
+ bot_name_ph: 'Bot name (starts with a letter, e.g. Accounting Bot)',
1049
1072
  bot_icon_ph: 'Pick an icon (emoji)',
1050
1073
  bot_created: 'Bot created:',
1051
1074
  bot_mem_sub: 'This bot\u2019s OWN memory — SOUL (personality) · USER (user info) · MEMORY (memory entries). Fully isolated from other bots.',
@@ -1075,6 +1098,7 @@
1075
1098
  bot_create: 'Create Bot',
1076
1099
  bot_cancel: 'Cancel',
1077
1100
  bot_name_req: 'Bot name is required',
1101
+ bot_name_letter: 'Bot name must START with a letter — first character must be a letter',
1078
1102
  p_preset: 'Built-in provider (endpoint auto-filled)',
1079
1103
  p_preset_custom: '— manual —',
1080
1104
  p_preset_hint: 'Pick a built-in provider to auto-fill the URL; just enter your API key and fetch models.',
@@ -77,6 +77,7 @@
77
77
  <button id="termCBtn" class="dd-btn dd-icon-btn" title="CMD terminali" data-i18n-title="tipTermC">&#x276F;&#xFE0E;</button>
78
78
  <button id="browserBtn" class="dd-btn dd-icon-btn" title="Dahili tarayıcı" data-i18n-title="tipBrowser">&#x29C9;</button>
79
79
  <button id="eyeBtn" class="dd-btn dd-icon-btn" title="Ajan tarayıcısı görünür/gizli — göz açıkken aramalar panelde izlenir, kapalıyken gizli çalışır" data-i18n-title="tipEye"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/><line class="eye-slash" x1="4" y1="4" x2="20" y2="20"/></svg></button>
80
+ <span id="netDot" class="dd-btn dd-icon-btn net-on" title="İnternet: bağlı" data-i18n-title="tipNet"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/><line class="net-slash" x1="4" y1="4" x2="20" y2="20"/></svg></span>
80
81
  <div class="drag-spacer"></div>
81
82
  </header>
82
83
 
@@ -40,6 +40,7 @@ const els = {
40
40
  todoPanel: $('#todoPanel'),
41
41
  browserBtn: $('#browserBtn'),
42
42
  eyeBtn: $('#eyeBtn'),
43
+ netDot: $('#netDot'),
43
44
  railBtn: $('#railBtn'),
44
45
  watchBtn: $('#watchBtn'),
45
46
  cronBtn: $('#cronBtn'),
@@ -199,6 +200,14 @@ let netOnline = true;
199
200
  let netQueueCount = 0;
200
201
  const netPending = []; // { key, el } — bekleyen mesaj balonları
201
202
 
203
+ /* wifi göstergesi: bağlı = yeşil, kopuk = kırmızı */
204
+ function paintNetDot() {
205
+ if (!els.netDot) return;
206
+ els.netDot.classList.toggle('net-on', netOnline);
207
+ els.netDot.classList.toggle('net-off', !netOnline);
208
+ els.netDot.title = _t(netOnline ? 'tip_net_online' : 'tip_net_offline');
209
+ }
210
+
202
211
  function setNetBadge(el, text) {
203
212
  if (!el || !el.isConnected) return;
204
213
  let badge = el.querySelector('.q-badge');
@@ -223,6 +232,7 @@ function onNetEvent(ev) {
223
232
  if (ev.type === 'net') {
224
233
  const was = netOnline;
225
234
  netOnline = ev.online !== false;
235
+ paintNetDot();
226
236
  if (was !== netOnline) {
227
237
  if (netOnline) {
228
238
  toast(_t('net_online'));
@@ -749,6 +759,50 @@ async function refreshFalloutPane() {
749
759
  renderFalloutPane();
750
760
  }
751
761
 
762
+ /* Obscura kurulum ilerlemesi: main process'te sürer — ayarlardan çıkılsa da
763
+ kesilmez. Panel açıksa çubuk canlı güncellenir; bitişte tek kez toast atılır. */
764
+ let _obscuraWasRunning = false;
765
+ function paintObscuraState(st) {
766
+ const ocSt = $('#ocStatus');
767
+ const ocProg = $('#ocProg');
768
+ const ocBtn = $('#ocInstall');
769
+ if (!ocSt || !ocProg || !ocBtn || !st || !st.phase) return;
770
+ const pct = Math.max(0, Math.min(100, Number(st.pct) || 0));
771
+ const fill = ocProg.querySelector('.oc-prog-fill');
772
+ const txt = ocProg.querySelector('.oc-prog-text');
773
+ if (st.running) {
774
+ ocBtn.disabled = true;
775
+ ocBtn.textContent = _t('oc_install_running');
776
+ ocProg.hidden = false;
777
+ if (fill) fill.style.width = pct + '%';
778
+ const phaseKey = ({ 'hazırlanıyor': 'prep', 'indiriliyor': 'download', 'kuruluyor': 'extract', 'doğrulanıyor': 'verify' })[st.phase];
779
+ if (txt) txt.textContent = (phaseKey ? _t('oc_phase_' + phaseKey) : st.phase) + ' — ' + pct + '%';
780
+ ocSt.textContent = _t('oc_bg_note');
781
+ } else if (st.phase === 'tamamlandı' && pct >= 100) {
782
+ ocBtn.disabled = false;
783
+ ocBtn.textContent = _t('oc_install');
784
+ ocProg.hidden = false;
785
+ if (fill) fill.style.width = '100%';
786
+ if (txt) txt.textContent = _t('oc_phase_done') + ' — 100%';
787
+ ocSt.textContent = _t('oc_status_ok');
788
+ } else if (st.phase === 'hata') {
789
+ ocBtn.disabled = false;
790
+ ocBtn.textContent = _t('oc_install');
791
+ ocProg.hidden = true;
792
+ ocSt.textContent = _t('oc_install_fail') + (st.error || '?');
793
+ }
794
+ }
795
+
796
+ function onObscuraProgress(ev) {
797
+ if (ev && ev.running) _obscuraWasRunning = true;
798
+ paintObscuraState(ev);
799
+ if (ev && !ev.running && _obscuraWasRunning) {
800
+ _obscuraWasRunning = false;
801
+ if (ev.phase === 'tamamlandı') toast(_t('oc_installed_toast'));
802
+ else if (ev.phase === 'hata') toast(_t('oc_install_fail') + (ev.error || '?'));
803
+ }
804
+ }
805
+
752
806
  /* Web Arama sekmesi: TinyFish anahtarı + Obscura (kurulum/aktiflik) + arama sırası */
753
807
  async function renderWebSearchPane() {
754
808
  const pane = $('#tab-websearch');
@@ -764,7 +818,10 @@ async function renderWebSearchPane() {
764
818
  '<div id="ocStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
765
819
  '<label class="mem-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">' +
766
820
  '<input id="ocEnabled" type="checkbox" style="width:auto" /> <span>' + _t('oc_enabled') + '</span></label>' +
767
- '<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:10px">' +
821
+ '<div id="ocProg" class="oc-prog" hidden>' +
822
+ '<div class="oc-prog-bar"><div class="oc-prog-fill" style="width:0%"></div></div>' +
823
+ '<div class="oc-prog-text sub"></div></div>' +
824
+ '<div style="display:flex;justify-content:center;margin-top:12px">' +
768
825
  '<button id="ocInstall" class="btn">' + _t('oc_install') + '</button></div>' +
769
826
  /* --- Arama sırası --- */
770
827
  '<div class="divider"></div>' +
@@ -786,22 +843,21 @@ async function renderWebSearchPane() {
786
843
  const ocSt = $('#ocStatus');
787
844
  const ocChk = $('#ocEnabled');
788
845
  const ocBtn = $('#ocInstall');
789
- const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '' }));
846
+ const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '', install: null }));
790
847
  ocChk.checked = oc.enabled !== false;
791
- ocSt.textContent = oc.installed ? _t('oc_status_ok') : _t('oc_status_missing');
848
+ /* kurulum arka planda sürüyorsa durum panelde GERİ YÜKLENİR —
849
+ ayarlardan çıkıp dönsen bile ilerleme kaybolmaz */
850
+ const ocState = oc.install || (await beast.obscuraInstallState().catch(() => null));
851
+ paintObscuraState(ocState);
792
852
  ocChk.addEventListener('change', async () => {
793
853
  await beast.obscuraSetEnabled(ocChk.checked).catch(() => {});
794
854
  toast(ocChk.checked ? _t('oc_on_toast') : _t('oc_off_toast'));
795
855
  });
796
856
  ocBtn.addEventListener('click', async () => {
797
- ocBtn.disabled = true;
798
- ocBtn.textContent = _t('oc_installing');
799
- ocSt.textContent = _t('oc_installing');
857
+ if (ocBtn.disabled) return;
800
858
  const r = await beast.obscuraInstall().catch(() => ({ ok: false, error: 'hata' }));
801
- ocBtn.disabled = false;
802
- ocBtn.textContent = _t('oc_install');
803
- ocSt.textContent = r && r.ok ? _t('oc_status_ok') : _t('oc_install_fail') + (r && r.error ? r.error : '?');
804
- toast(r && r.ok ? _t('oc_installed_toast') : _t('oc_install_fail') + (r && r.error ? r.error : '?'));
859
+ if (r && (r.ok || r.started || r.busy)) paintObscuraState(await beast.obscuraInstallState().catch(() => null));
860
+ else toast(_t('oc_install_fail') + ((r && r.error) || '?'));
805
861
  });
806
862
 
807
863
  /* --- Arama sırası --- */
@@ -3208,8 +3264,11 @@ function renderBotSettings(pane, b) {
3208
3264
  })
3209
3265
  );
3210
3266
  $('#bSave').addEventListener('click', async () => {
3267
+ const newName = $('#bName').value.trim();
3268
+ /* İLK HARF ZORUNLU: ad değiştirilirken de harf karakteriyle başlamalı */
3269
+ if (newName && !/^\p{L}/u.test(newName)) { toast(_t('bot_name_letter')); $('#bName').focus(); return; }
3211
3270
  const patch = {
3212
- name: $('#bName').value.trim(),
3271
+ name: newName,
3213
3272
  icon: (f.querySelector('#bIconPick button.on') || {}).dataset?.ic || b.icon,
3214
3273
  prompt: $('#bPrompt').value,
3215
3274
  seeBots: [...f.querySelectorAll('#bSee input:checked')].map((c) => c.dataset.see),
@@ -3235,8 +3294,12 @@ function renderBotSettings(pane, b) {
3235
3294
  if (delBtn) delBtn.addEventListener('click', async () => {
3236
3295
  if (!confirm(_ti('bot_confirm_del', b.name))) return;
3237
3296
  const r = await beast.botsRemove(b.id);
3238
- if (r.ok) { toast(_t('bot_deleted')); botPageId = null; await refreshBots(); renderBotPage(); }
3239
- else toast(r.error || _t('bot_save_fail'));
3297
+ if (r.ok) {
3298
+ /* bot silme sonrası main otomatik restart atar — yeniden çizmeye kalkışma */
3299
+ toast(r.restarting ? _t('bot_deleted_restart') : _t('bot_deleted'));
3300
+ botPageId = null;
3301
+ if (!r.restarting) { await refreshBots(); renderBotPage(); }
3302
+ } else toast(r.error || _t('bot_save_fail'));
3240
3303
  });
3241
3304
  /* (numara ekleme alanı kaldırıldı — Entegrasyonlar üzerinden yapılır) */
3242
3305
  }
@@ -3358,6 +3421,8 @@ function renderBotAdd(pane) {
3358
3421
  $('#bAddCreate').addEventListener('click', async () => {
3359
3422
  const name = $('#bAddName').value.trim();
3360
3423
  if (!name) { toast(_t('bot_name_req')); $('#bAddName').focus(); return; }
3424
+ /* İLK HARF ZORUNLU: bot adı harf karakteriyle başlamalı */
3425
+ if (!/^\p{L}/u.test(name)) { toast(_t('bot_name_letter')); $('#bAddName').focus(); return; }
3361
3426
  const icon = (f.querySelector('#bAddIconPick button.on') || {}).dataset?.ic || '🤖';
3362
3427
  const r = await beast.botsAdd({ name, icon, prompt: $('#bAddPrompt').value });
3363
3428
  if (r.ok) {
@@ -3715,6 +3780,8 @@ function onEvent(ev) {
3715
3780
  }
3716
3781
  /* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
3717
3782
  if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
3783
+ /* Obscura kurulum ilerlemesi — ayarlardan çıkıp dönülsen de main'de sürer */
3784
+ if (ev.type === 'obscura-progress') { onObscuraProgress(ev); return; }
3718
3785
  /* Beast Code oturumu (IDE modu ortasındaki panel): olayları panele akıt,
3719
3786
  ana sohbeti kirletme */
3720
3787
  if (bcSessionId && ev.sessionId === bcSessionId) {
@@ -4301,6 +4368,7 @@ function autosize() {
4301
4368
 
4302
4369
  const SLASH_COMMANDS = [
4303
4370
  { cmd: '/help', desc: 'tüm komutları listele' },
4371
+ { cmd: '/version', desc: 'Beast Agent sürümünü göster' },
4304
4372
  { cmd: '/new', desc: 'yeni oturum aç (kod verilir)' },
4305
4373
  { cmd: '/open ', desc: 'koddaki oturuma geç' },
4306
4374
  { cmd: '/sessions', desc: 'bu sohbetin oturumları' },
@@ -291,6 +291,13 @@ body.browser-open #topbar { width: auto; }
291
291
  #eyeBtn:not(.on) { opacity: 0.55; }
292
292
  #eyeBtn.on { background: var(--accent-dim); color: var(--accent); }
293
293
 
294
+ /* internet göstergesi (topbar sağ uç): bağlı = YEŞİL wifi, kopuk = KIRMIZI */
295
+ #netDot { cursor: default; }
296
+ #netDot.net-on { color: #22c55e; }
297
+ #netDot.net-off { color: #ef4444; }
298
+ #netDot.net-off .net-slash { display: block; }
299
+ #netDot.net-on .net-slash { display: none; }
300
+
294
301
  /* #19 sağ panel (paralel ajan konsolu) aç/kapa */
295
302
  #railBtn.on { background: var(--accent-dim); color: var(--accent); }
296
303
  body.rail-hidden #rail { display: none; }
@@ -770,6 +777,23 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
770
777
  word-break: break-word;
771
778
  }
772
779
 
780
+ /* Obscura kurulum ilerleme çubuğu (Ayarlar → Web Arama) */
781
+ .oc-prog { margin-top: 10px; }
782
+ .oc-prog-bar {
783
+ height: 9px;
784
+ background: var(--accent-dim);
785
+ border: 1px solid var(--border);
786
+ border-radius: 6px;
787
+ overflow: hidden;
788
+ }
789
+ .oc-prog-fill {
790
+ height: 100%;
791
+ width: 0%;
792
+ background: var(--accent);
793
+ transition: width 0.25s ease;
794
+ }
795
+ .oc-prog-text { margin-top: 5px; font-size: 12px; color: var(--muted); }
796
+
773
797
  /* markdown */
774
798
  .md p { margin: 0 0 8px; }
775
799
  .md p:last-child { margin-bottom: 0; }
@@ -1784,14 +1808,16 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
1784
1808
  flex-shrink: 0;
1785
1809
  }
1786
1810
 
1787
- /* tek tıkla model (OpenCode Zen) hero butonu */
1811
+ /* tek tıkla model (OpenCode Zen) hero butonu — buton ortada */
1788
1812
  .zen-hero {
1789
1813
  padding: 10px 12px;
1790
1814
  border: 1px dashed var(--border);
1791
1815
  border-radius: 10px;
1792
1816
  margin-bottom: 12px;
1793
1817
  background: var(--panel);
1818
+ text-align: center;
1794
1819
  }
1820
+ .zen-hero .sub { text-align: left; }
1795
1821
  .zen-hero .btn { min-width: 220px; }
1796
1822
  .zen-hero .btn .zen-load {
1797
1823
  display: inline-block;
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ /* Bot adı disiplini: İLK KARAKTER HARF ZORUNLU (ekleme + güncelleme) */
4
+
5
+ const test = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+
11
+ /* BEAST_DATA modül yüklenmeden önce ayarlanmalı (REG cache'i tek dosyada) */
12
+ process.env.BEAST_DATA = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-bots-name-'));
13
+ const bots = require('../src/agent/bots');
14
+
15
+ test('bot ekleme: ad harf ile başlamalı', () => {
16
+ for (const bad of ['1Muhasebe', '9bot', '-abc', '_x', ' 3BoT']) {
17
+ const r = bots.add({ name: bad, prompt: '' });
18
+ assert.strictEqual(r.ok, false, bad + ' reddedilmeli');
19
+ assert.ok(/harf ile başlamalı/.test(r.error), r.error);
20
+ }
21
+ const okNum = bots.add({ name: 'Muhasebe', prompt: '' });
22
+ assert.ok(okNum.ok, 'harf ile başlayan ad kabul: ' + (okNum.error || ''));
23
+ const okTr = bots.add({ name: 'Çağrı', prompt: '' });
24
+ assert.ok(okTr.ok, 'Türkçe harf (Ç) kabul: ' + (okTr.error || ''));
25
+ });
26
+
27
+ test('bot güncelleme: geçersiz ad diğer alanlara dokunmadan reddedilir', () => {
28
+ const created = bots.add({ name: 'Depo', prompt: 'eski' });
29
+ assert.ok(created.ok);
30
+ const id = created.bot.id;
31
+
32
+ const bad = bots.update(id, { name: '3Depo', prompt: 'yeni' });
33
+ assert.strictEqual(bad.ok, false, 'geçersiz ad reddedildi');
34
+ const after = bots.get(id);
35
+ assert.strictEqual(after.name, 'Depo', 'eski ad korundu');
36
+ assert.strictEqual(after.prompt, 'eski', 'prompt değişmedi');
37
+
38
+ const good = bots.update(id, { name: 'Anbar2', prompt: 'yeni' });
39
+ assert.ok(good.ok, 'harf ile başlayan güncelleme kabul: ' + (good.error || ''));
40
+ assert.strictEqual(bots.get(id).prompt, 'yeni');
41
+ assert.strictEqual(bots.get(id).name, 'Anbar2');
42
+ });
package/tests/llm.test.js CHANGED
@@ -3,7 +3,7 @@
3
3
  require('./setup');
4
4
  const test = require('node:test');
5
5
  const assert = require('node:assert');
6
- const { withRetries, friendlyError, sumUsage, chatStreamAuto } = require('../src/agent/llm');
6
+ const { withRetries, friendlyError, sumUsage, chatStreamAuto, setNetProbe, isNetworkError } = require('../src/agent/llm');
7
7
 
8
8
  test('withRetries: 503 sonrası başarılı isteği tekrarlar', async () => {
9
9
  let calls = 0;
@@ -134,3 +134,72 @@ test('chatStreamAuto: normal (stop) yanıtta hiç devam turu açılmaz', async (
134
134
  srv.close();
135
135
  }
136
136
  });
137
+
138
+ /* ---------- ağ kopması dayanıklılığı (#retry) ---------- */
139
+
140
+ test('isNetworkError sınıflandırması', () => {
141
+ const net = new Error('fetch failed');
142
+ assert.equal(isNetworkError(net), true, 'status yok → ağ hatası');
143
+ const http = new Error('HTTP 503');
144
+ http.status = 503;
145
+ assert.equal(isNetworkError(http), false, 'status var → HTTP hatası');
146
+ const abort = new Error('iptal');
147
+ abort.name = 'AbortError';
148
+ assert.equal(isNetworkError(abort), false, 'abort ağ hatası sayılmaz');
149
+ assert.equal(isNetworkError(null), false);
150
+ });
151
+
152
+ test('withRetries: internet kapalıyken probe bekler, dönünce başarır', async () => {
153
+ let online = false;
154
+ setNetProbe(() => online);
155
+ setTimeout(() => { online = true; }, 30);
156
+ let calls = 0;
157
+ const t0 = Date.now();
158
+ const r = await withRetries(async () => {
159
+ calls++;
160
+ if (calls === 1) throw new Error('fetch failed');
161
+ return 'ok';
162
+ });
163
+ assert.equal(r, 'ok');
164
+ assert.equal(calls, 2);
165
+ assert.ok(Date.now() - t0 > 2000, 'probe çevrimdışı dediği için bekledi');
166
+ setNetProbe(null);
167
+ });
168
+
169
+ test('akış ortasında bağlantı koparsa: kısmi metinle devam turu açılır (hata balonu yok)', async () => {
170
+ const http = require('node:http');
171
+ const calls = [];
172
+ const srv = http.createServer((req, res) => {
173
+ let body = '';
174
+ req.on('data', (c) => (body += c));
175
+ req.on('end', () => {
176
+ calls.push(JSON.parse(body).messages);
177
+ if (calls.length === 1) {
178
+ /* birkaç delta yaz, sonra soketi ORTADAN kopar */
179
+ res.writeHead(200, { 'Content-Type': 'text/event-stream' });
180
+ res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'Merhaba du' } }] }) + '\n\n');
181
+ setTimeout(() => res.destroy(), 30);
182
+ return;
183
+ }
184
+ res.writeHead(200, { 'Content-Type': 'text/event-stream' });
185
+ res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'nya devam' } }] }) + '\n\n');
186
+ res.write('data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\n\n');
187
+ res.write('data: [DONE]\n\n');
188
+ res.end();
189
+ });
190
+ });
191
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
192
+ try {
193
+ const port = srv.address().port;
194
+ const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
195
+ const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] });
196
+ assert.equal(r.content, 'Merhaba dunya devam', 'kısmi + devam birleşti');
197
+ assert.equal(r.finishReason, 'stop');
198
+ assert.equal(calls.length, 2, 'devam turu açıldı');
199
+ const m2 = calls[1];
200
+ assert.equal(m2[m2.length - 2].role, 'assistant');
201
+ assert.match(m2[m2.length - 1].content, /DEVAM/);
202
+ } finally {
203
+ srv.close();
204
+ }
205
+ });