beast-agent 0.16.0 → 0.17.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/.env.example CHANGED
@@ -3,5 +3,6 @@
3
3
  # Bu dosya config.yaml'daki key_env alanlarıyla eşleşir;
4
4
  # key_env yoksa <PROVIDER_ID>_API_KEY denenir (örn. zhipu -> ZHIPU_API_KEY).
5
5
 
6
+ OPENCODE_API_KEY=
6
7
  ZHIPU_API_KEY=
7
8
  OPENAI_API_KEY=
package/README.md CHANGED
@@ -27,8 +27,6 @@ Beast Agent is a personal AI agent that runs on your machine and connects to **a
27
27
 
28
28
  ## 📦 Download & Install — 2 commands, ready
29
29
 
30
- ### Option 1 · npm (recommended)
31
-
32
30
  ```bash
33
31
  npm install -g beast-agent
34
32
  ```
@@ -41,15 +39,33 @@ beast-agent
41
39
 
42
40
  That's it — the app window opens. On first launch Beast also creates a **desktop shortcut** and registers itself to **start with Windows** (lives in the tray). Later updates: close the app and run `beast-agent update`.
43
41
 
44
- ### Option 2 · Installer
42
+ ## ⚙️ Configuration
45
43
 
46
- Grab `BeastAgent-Setup-x.x.x.exe` from the [Releases](../../releases) page installs with a desktop shortcut and auto-start. A portable `BeastAgent.exe` (no install) is also available there.
44
+ ### Recommended: OpenCode Zen (free tier, no credit card)
47
45
 
48
- **Website:** [beast.algokod.com](https://beast.algokod.com)
46
+ Get a key at [opencode.ai/auth](https://opencode.ai/auth), then copy `config.example.yaml` → `config.yaml`:
49
47
 
50
- ## ⚙️ Configuration
48
+ ```yaml
49
+ defaultSelection: opencode::glm-5.2
50
+ providers:
51
+ - id: opencode
52
+ name: OpenCode Zen
53
+ baseUrl: https://opencode.ai/zen/v1
54
+ apiKey: <your-key>
55
+ models:
56
+ - glm-5.2
57
+ - kimi-k2.7-code
58
+ - deepseek-v4-flash-free
59
+ - big-pickle
60
+ - minimax-m3
61
+ ```
62
+
63
+ > Free models include `deepseek-v4-flash-free`, `big-pickle`, `mimo-v2.5-free`, `nemotron-3-ultra-free`. If you have an **OpenCode Go** subscription, use the same config with `baseUrl: https://opencode.ai/zen/go/v1`.
64
+
65
+ ### Any other OpenAI-compatible provider
66
+
67
+ OpenRouter, Zhipu, Ollama, OpenAI — anything that speaks `/v1/chat/completions` works:
51
68
 
52
- 1. Copy `config.example.yaml` → `config.yaml` and add your providers:
53
69
  ```yaml
54
70
  defaultSelection: openrouter::anthropic/claude-3.5-sonnet
55
71
  providers:
@@ -61,8 +77,8 @@ providers:
61
77
  - anthropic/claude-3.5-sonnet
62
78
  - gpt-4o
63
79
  ```
64
- 2. You can also provide API keys via `.env`: copy `.env.example` → `.env`
65
- 3. On first launch, Settings → Provider pulls your models automatically.
80
+
81
+ API keys can also go into `.env` (copy `.env.example` → `.env`). On first launch, Settings → Provider pulls your models automatically.
66
82
 
67
83
  All app data lives under `%APPDATA%\beast` (sessions, memory, WhatsApp pairing, encrypted settings).
68
84
 
@@ -2,7 +2,20 @@
2
2
  # Kullanım: bu dosyayı %APPDATA%\beast\config.yaml olarak kopyala ve düzenle.
3
3
  # (Taşınabilir kullanım için BEAST_DATA ortam değişkeni ile başka klasöre alabilirsin.)
4
4
 
5
+ # ÖNERİLEN: OpenCode Zen — ücretsiz modeller, kredi kartı gerekmez (opencode.ai/auth)
5
6
  providers:
7
+ opencode:
8
+ name: OpenCode Zen
9
+ base_url: https://opencode.ai/zen/v1
10
+ key_env: OPENCODE_API_KEY
11
+ models:
12
+ glm-5.2: {}
13
+ kimi-k2.7-code: {}
14
+ deepseek-v4-flash-free: {}
15
+ big-pickle: {}
16
+ minimax-m3: {}
17
+ # OpenCode Go aboneliğin varsa base_url: https://opencode.ai/zen/go/v1 kullan.
18
+
6
19
  zhipu:
7
20
  name: Zhipu AI
8
21
  base_url: https://api.z.ai/api/paas/v4
@@ -20,7 +33,7 @@ providers:
20
33
  gpt-4o: {}
21
34
 
22
35
  model:
23
- provider: zhipu # varsayılan provider id (yoksa ilk provider kullanılır)
24
- default: glm-4.6 # varsayılan model
36
+ provider: opencode # varsayılan provider id (yoksa ilk provider kullanılır)
37
+ default: glm-5.2 # varsayılan model
25
38
  # base_url: ... # istersen aktif provider'ın URL'ini override et
26
39
  # key_env: ... # istersen anahtarı farklı env'den oku
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "0.16.0",
4
+ "version": "0.17.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
package/src/main.js CHANGED
@@ -30,6 +30,33 @@ function isNpmMode() {
30
30
  return !app.isPackaged && /node_modules[\\/]beast-agent/i.test(String(app.getAppPath()));
31
31
  }
32
32
 
33
+ /* npm registry'den en son sürüm (10 dk cache) — Update sekmesi + otomatik kontrol */
34
+ let npmLatestCache = { version: null, at: 0 };
35
+
36
+ function isNewerVersion(a, b) {
37
+ const pa = String(a).split('.').map(Number);
38
+ const pb = String(b).split('.').map(Number);
39
+ for (let i = 0; i < 3; i++) {
40
+ const d = (pa[i] || 0) - (pb[i] || 0);
41
+ if (d) return d > 0;
42
+ }
43
+ return false;
44
+ }
45
+
46
+ async function getNpmLatest(force) {
47
+ const now = Date.now();
48
+ if (!force && npmLatestCache.version && now - npmLatestCache.at < 10 * 60 * 1000) return npmLatestCache.version;
49
+ try {
50
+ const res = await fetch('https://registry.npmjs.org/beast-agent/latest', { signal: AbortSignal.timeout(15000) });
51
+ if (res.ok) {
52
+ const j = await res.json();
53
+ const v = String(j.version || '').trim();
54
+ if (v) npmLatestCache = { version: v, at: now };
55
+ }
56
+ } catch {}
57
+ return npmLatestCache.version;
58
+ }
59
+
33
60
  function emitUpdateEvent() {
34
61
  try {
35
62
  if (win && !win.isDestroyed()) {
@@ -99,6 +126,24 @@ function startAutoUpdater() {
99
126
  }
100
127
  } catch {}
101
128
  }
129
+
130
+ /* npm kurulumu için otomatik sürüm kontrolü (registry üzerinden, 6 saatte bir) */
131
+ function startNpmUpdateWatch() {
132
+ if (!isNpmMode() || settings.autoCheckUpdate === false) return;
133
+ const check = async () => {
134
+ try {
135
+ const v = await getNpmLatest();
136
+ if (v && isNewerVersion(v, app.getVersion()) && !updateState.available) {
137
+ updateState.available = true;
138
+ updateState.version = v;
139
+ emitUpdateEvent();
140
+ log.info('main', `npm: yeni sürüm var ${v} (mevcut ${app.getVersion()})`);
141
+ }
142
+ } catch {}
143
+ };
144
+ check();
145
+ setInterval(check, 6 * 60 * 60 * 1000);
146
+ }
102
147
  const { htmlToText, setExaKey, setTinyfishKey } = require('./agent/tools');
103
148
  const { waToolLine } = require('./agent/watext');
104
149
 
@@ -460,12 +505,17 @@ function decodeAudioToPcm16k(buf) {
460
505
  });
461
506
  }
462
507
 
463
- async function transcribeAudio(buf /* , mimetype */) {
508
+ async function transcribeAudio(buf, langOverride /* 'tr' | 'en' | 'auto' */) {
464
509
  try {
465
510
  const audio = await decodeAudioToPcm16k(buf);
466
511
  if (!audio || !audio.length) return null;
467
512
  const asr = await ensureStt();
468
- const out = await asr(audio, { language: 'turkish', task: 'transcribe', chunk_length_s: 30, stride_length_s: 5 });
513
+ /* dil: arayüz diline bağlı UI Türkçe ise Türkçe, İngilizce ise İngilizce algılar */
514
+ const lang = langOverride || settings.sttLang || 'tr';
515
+ const opts = { task: 'transcribe', chunk_length_s: 30, stride_length_s: 5 };
516
+ if (lang === 'en') opts.language = 'english';
517
+ else if (lang === 'tr') opts.language = 'turkish';
518
+ const out = await asr(audio, opts);
469
519
  return String((out && out.text) || '').trim() || null;
470
520
  } catch (e) {
471
521
  waLog('stt hata: ' + String((e && e.message) || e));
@@ -1740,6 +1790,7 @@ app.whenReady().then(() => {
1740
1790
  maybeRunWhereWasI();
1741
1791
  falloutResume();
1742
1792
  startAutoUpdater(); // #3 sessiz güncelleme
1793
+ startNpmUpdateWatch(); // npm kurulumunda registry üzerinden otomatik sürüm kontrolü
1743
1794
 
1744
1795
  // #12 STT prefetch: whisper modelini arka planda hazırla (ilk sesli mesajda bekleme olmasın)
1745
1796
  if (settings.sttPrefetch !== false) {
@@ -3135,25 +3186,48 @@ ipcMain.handle('sec:set', (_e, cfg) => {
3135
3186
  ipcMain.handle('approval:respond', (_e, { id, ok, always }) => resolveApproval(id, ok, always));
3136
3187
 
3137
3188
  /* ---------------- #Update IPC ---------------- */
3138
- ipcMain.handle('update:status', () => ({
3139
- current: app.getVersion(),
3140
- packaged: app.isPackaged,
3141
- npm: isNpmMode(),
3142
- ...updateState,
3143
- autoCheck: settings.autoCheckUpdate !== false,
3144
- autoDownload: settings.autoDownloadUpdate !== false,
3145
- }));
3146
-
3147
- ipcMain.handle('update:check', async (_e, viaCommand) => {
3148
- if (isNpmMode()) return { ok: false, npm: true, error: 'npm kurulumu — güncelleme: beast-agent update' };
3149
- if (!autoUpdater) return { ok: false, error: 'updater kullanılamıyor (taşınabilir/geliştirme modu)' };
3189
+ ipcMain.handle('update:status', async () => {
3190
+ const st = {
3191
+ current: app.getVersion(),
3192
+ packaged: app.isPackaged,
3193
+ npm: isNpmMode(),
3194
+ ...updateState,
3195
+ autoCheck: settings.autoCheckUpdate !== false,
3196
+ autoDownload: settings.autoDownloadUpdate !== false,
3197
+ };
3150
3198
  try {
3151
- const r = await autoUpdater.checkForUpdates();
3152
- const v = r && r.update && r.update.version;
3153
- return { ok: true, version: v, available: !!v && v !== app.getVersion() };
3154
- } catch (e) {
3155
- return { ok: false, error: String((e && e.message) || e) };
3199
+ /* npm registry'den güncel sürüm — updater ne yaparsa yapsın "En son sürüm" kartı dolu olsun */
3200
+ const v = await getNpmLatest();
3201
+ if (v) {
3202
+ st.npmLatest = v;
3203
+ if (!st.version) st.version = v;
3204
+ if (isNewerVersion(v, app.getVersion())) st.available = true;
3205
+ }
3206
+ } catch {}
3207
+ return st;
3208
+ });
3209
+
3210
+ ipcMain.handle('update:check', async () => {
3211
+ const current = app.getVersion();
3212
+ /* her modda npm registry'den taze kontrol (cache bypass) */
3213
+ const v = await getNpmLatest(true);
3214
+ const out = { ok: true, npm: isNpmMode(), version: v || undefined, available: v ? isNewerVersion(v, current) : false };
3215
+ if (v) {
3216
+ if (!updateState.version || isNewerVersion(v, updateState.version)) updateState.version = v;
3217
+ if (isNewerVersion(v, current)) updateState.available = true;
3156
3218
  }
3219
+ /* packaged + updater: GitHub Releases kontrolü de çalışsın */
3220
+ if (!isNpmMode() && autoUpdater) {
3221
+ try {
3222
+ const r = await autoUpdater.checkForUpdates();
3223
+ const uv = r && r.update && r.update.version;
3224
+ if (uv) { out.version = uv; out.available = uv !== current; }
3225
+ } catch (e) {
3226
+ if (!v) return { ok: false, error: String((e && e.message) || e) };
3227
+ }
3228
+ }
3229
+ emitUpdateEvent();
3230
+ return out;
3157
3231
  });
3158
3232
 
3159
3233
  ipcMain.handle('update:install', () => {
@@ -3191,17 +3265,26 @@ async function runUpdateCommand(reply /* fn(text) */) {
3191
3265
  }
3192
3266
 
3193
3267
  /* #STT: sohbet mikrofonu — MediaRecorder sesini (webm/opus) yerel whisper'a çevir */
3194
- ipcMain.handle('stt:transcribe', async (_e, b64) => {
3268
+ /* #STT: sohbet mikrofonu — MediaRecorder sesini (webm/opus) yerel whisper'a çevir */
3269
+ ipcMain.handle('stt:transcribe', async (_e, b64, lang) => {
3195
3270
  try {
3196
3271
  const buf = Buffer.from(String(b64 || '').split(',').pop() || '', 'base64');
3197
3272
  if (!buf.length) return { ok: false, error: 'boş ses kaydı' };
3198
- const text = await transcribeAudio(buf);
3273
+ const text = await transcribeAudio(buf, lang === 'en' || lang === 'auto' ? lang : undefined);
3199
3274
  return text ? { ok: true, text } : { ok: false, error: 'konuşma algılanamadı' };
3200
3275
  } catch (e) {
3201
3276
  return { ok: false, error: String((e && e.message) || e) };
3202
3277
  }
3203
3278
  });
3204
3279
 
3280
+ ipcMain.handle('stt:lang:get', () => settings.sttLang || 'tr');
3281
+ ipcMain.handle('stt:lang:set', (_e, lang) => {
3282
+ const v = ['auto', 'tr', 'en'].includes(String(lang)) ? String(lang) : 'tr';
3283
+ settings.sttLang = v;
3284
+ saveSettings();
3285
+ return v;
3286
+ });
3287
+
3205
3288
  ipcMain.handle('limits:get', () => {
3206
3289
  const l = settings.limits || {};
3207
3290
  return {
package/src/preload.js CHANGED
@@ -44,7 +44,9 @@ contextBridge.exposeInMainWorld('beast', {
44
44
  secGet: () => ipcRenderer.invoke('sec:get'),
45
45
  secSet: (cfg) => ipcRenderer.invoke('sec:set', cfg),
46
46
  approvalRespond: (id, ok, always) => ipcRenderer.invoke('approval:respond', { id, ok, always }),
47
- sttTranscribe: (b64) => ipcRenderer.invoke('stt:transcribe', b64),
47
+ sttTranscribe: (b64, lang) => ipcRenderer.invoke('stt:transcribe', b64, lang),
48
+ sttLangGet: () => ipcRenderer.invoke('stt:lang:get'),
49
+ sttLangSet: (lang) => ipcRenderer.invoke('stt:lang:set', lang),
48
50
  updateStatus: () => ipcRenderer.invoke('update:status'),
49
51
  updateCheck: () => ipcRenderer.invoke('update:check'),
50
52
  updateInstall: () => ipcRenderer.invoke('update:install'),
@@ -93,6 +93,7 @@
93
93
  <div id="settingsDialog">
94
94
  <aside id="setTabs">
95
95
  <div class="set-tabs-title" data-i18n="set_title">AYARLAR</div>
96
+ <nav class="set-tabs-list">
96
97
  <button class="tab active" data-tab="provider" data-i18n="tab_provider">Provider</button>
97
98
  <button class="tab" data-tab="fallout" data-i18n="tab_fallout">Fallout</button>
98
99
  <button class="tab" data-tab="memory" data-i18n="tab_memory">Memory</button>
@@ -110,6 +111,7 @@
110
111
  <button class="tab" data-tab="limits" data-i18n="tab_limits">Limit</button>
111
112
  <button class="tab" data-tab="sec" data-i18n="tab_security">Güvenlik</button>
112
113
  <button class="tab" data-tab="update" data-i18n="tab_update">Güncelleme</button>
114
+ </nav>
113
115
  <div class="set-tabs-foot">
114
116
  <div id="beastCodeBox" class="beast-code" data-i18n-title="bc_label" title="Beast Kodu">
115
117
  <div class="bc-row">
@@ -1550,16 +1550,16 @@ let updatePaneTimer = null;
1550
1550
 
1551
1551
  function renderUpdateStateHtml(st) {
1552
1552
  let status;
1553
- if (st.npm) status = _t('up_npm_mode');
1554
- else if (st.error) status = '⚠ ' + st.error;
1553
+ if (st.error) status = '\u26A0\uFE0E ' + st.error;
1555
1554
  else if (st.downloaded) status = _t('up_downloaded') + ' (v' + (st.version || '?') + ')';
1556
1555
  else if (st.progress) status = _t('up_downloading') + ' %' + st.progress.percent;
1557
1556
  else if (st.checking) status = _t('up_checking');
1558
1557
  else if (st.available) status = _t('up_available') + ' (v' + (st.version || '?') + ')';
1558
+ else if (st.npm) status = _t('up_npm_mode');
1559
1559
  else if (st.available === false) status = _t('up_uptodate');
1560
1560
  else status = '—';
1561
1561
 
1562
- return `<div class="usage-stat" style="margin-top:10px"><div class="us-label">${_t('up_status')}</div><div class="us-value" style="font-size:14px">${escapeHtml(status)}</div></div>`;
1562
+ return `<div class="usage-stat" id="upStatusBox"><div class="us-label">${_t('up_status')}</div><div class="us-value" style="font-size:14px">${escapeHtml(status)}</div></div>`;
1563
1563
  }
1564
1564
 
1565
1565
  async function renderUpdatePane() {
@@ -1599,7 +1599,9 @@ beast-agent update</pre></div>`
1599
1599
  if (btnCheck) btnCheck.addEventListener('click', async () => {
1600
1600
  btnCheck.disabled = true;
1601
1601
  const r = await beast.updateCheck().catch(() => ({ ok: false, error: 'ipc' }));
1602
+ btnCheck.disabled = false;
1602
1603
  if (!r.ok && r.error) toast(r.error);
1604
+ renderUpdatePane();
1603
1605
  });
1604
1606
  const btnInstall = pane.querySelector('#upInstall');
1605
1607
  if (btnInstall) btnInstall.addEventListener('click', async () => {
@@ -1607,16 +1609,13 @@ beast-agent update</pre></div>`
1607
1609
  if (!r.ok && r.error) toast(r.error);
1608
1610
  });
1609
1611
 
1610
- /* indirme ilerlemesi için sekme açıkken canlı tazele */
1612
+ /* indirme ilerlemesi için sekme açıkken canlı tazele — YALNIZ durum kutusu */
1611
1613
  updatePaneTimer = setInterval(() => {
1612
1614
  if ($('#tab-update').hidden || els.settingsOverlay.hidden) { clearInterval(updatePaneTimer); return; }
1613
1615
  beast.updateStatus().then((s) => {
1614
1616
  if (!s) return;
1615
- const box = pane.querySelector('.us-value');
1616
- if (box) {
1617
- const wrap = pane.querySelector('.usage-stat');
1618
- if (wrap) wrap.outerHTML = renderUpdateStateHtml(s);
1619
- }
1617
+ const box = pane.querySelector('#upStatusBox');
1618
+ if (box) box.outerHTML = renderUpdateStateHtml(s);
1620
1619
  }).catch(() => {});
1621
1620
  }, 1000);
1622
1621
  }
@@ -2194,6 +2193,7 @@ function onEvent(ev) {
2194
2193
  }
2195
2194
  if (ev.type === 'update') {
2196
2195
  if (ev.downloaded) toast(_t('up_downloaded') + ' (v' + (ev.version || '?') + ') — /update now');
2196
+ else if (ev.available && ev.version && ev.version !== ev.current) toast(_t('up_available') + ' (v' + ev.version + ')');
2197
2197
  if (!els.settingsOverlay.hidden && setTab === 'update') renderUpdatePane();
2198
2198
  return;
2199
2199
  }
@@ -3051,6 +3051,10 @@ async function init() {
3051
3051
  });
3052
3052
  document.addEventListener('langchange', syncLangBtn);
3053
3053
  document.addEventListener('langchange', renderActiveSettingsTab);
3054
+ /* Whisper STT dili arayüz dilini izler (WA sesli notları da) */
3055
+ const syncSttLang = () => { try { beast.sttLangSet((window.I18N && window.I18N.lang) || 'tr'); } catch {} };
3056
+ syncSttLang();
3057
+ document.addEventListener('langchange', syncSttLang);
3054
3058
  }
3055
3059
 
3056
3060
  els.attachBtn.addEventListener('click', () => els.fileInput.click());
@@ -3091,7 +3095,9 @@ async function init() {
3091
3095
  if (!blob.size) return;
3092
3096
  toast(_t('mic_transcribing'));
3093
3097
  const b64 = await blobToDataUrl(blob);
3094
- const r = await beast.sttTranscribe(b64).catch(() => ({ ok: false, error: 'ipc' }));
3098
+ /* arayüz dili = Whisper dili: TR ise Türkçe, EN ise İngilizce algılar */
3099
+ const uiLang = (window.I18N && window.I18N.lang) || 'tr';
3100
+ const r = await beast.sttTranscribe(b64, uiLang).catch(() => ({ ok: false, error: 'ipc' }));
3095
3101
  if (r && r.ok && r.text) {
3096
3102
  els.input.value = (els.input.value ? els.input.value.replace(/\s+$/, '') + ' ' : '') + r.text;
3097
3103
  els.input.focus();
@@ -1096,11 +1096,33 @@ body.browser-open #toast { left: calc((100vw - var(--bw, 480px)) / 2); }
1096
1096
  flex-direction: column;
1097
1097
  padding: 16px 10px 12px;
1098
1098
  }
1099
- .set-tabs-title {
1100
- font-size: 11px;
1101
- letter-spacing: 2px;
1102
- color: var(--muted);
1103
- padding: 0 12px 10px;
1099
+ .set-tabs-title {
1100
+ font-size: 11px;
1101
+ letter-spacing: 2px;
1102
+ color: var(--muted);
1103
+ padding: 0 12px 10px;
1104
+ }
1105
+ /* sekme listesi: küçük ekranda taşarsa içeride kayar — alt kısım (Beast Kodu + Kapat) sabit kalır */
1106
+ .set-tabs-list {
1107
+ display: flex;
1108
+ flex-direction: column;
1109
+ align-items: stretch;
1110
+ flex: 1 1 auto;
1111
+ min-height: 0;
1112
+ overflow-y: auto;
1113
+ padding-right: 2px;
1114
+ margin: 0 -2px;
1115
+ scrollbar-width: thin;
1116
+ scrollbar-color: var(--border) transparent;
1117
+ }
1118
+ .set-tabs-list::-webkit-scrollbar { width: 8px; }
1119
+ .set-tabs-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; }
1120
+ .set-tabs-list::-webkit-scrollbar-thumb:hover { background: var(--muted); }
1121
+ .set-tabs-list::-webkit-scrollbar-track { background: transparent; }
1122
+ @media (max-height: 860px) {
1123
+ #setTabs { padding: 10px 8px; }
1124
+ .set-tabs-title { padding: 0 12px 6px; }
1125
+ #setTabs .tab { padding: 6px 12px; font-size: 12.5px; margin-bottom: 1px; }
1104
1126
  }
1105
1127
  #setTabs .tab {
1106
1128
  text-align: left;