beast-agent 0.26.0 → 0.26.2
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/bin/beast-agent.js +34 -17
- package/package.json +2 -2
- package/scripts/fix-electron.js +126 -0
- package/src/agent/bots.js +8 -0
- package/src/agent/discord.js +268 -0
- package/src/agent/engine.js +135 -77
- package/src/agent/llm.js +46 -17
- package/src/agent/research.js +133 -0
- package/src/agent/skills.js +8 -1
- package/src/agent/tools.js +53 -14
- package/src/agent/watext.js +4 -0
- package/src/main.js +534 -351
- package/src/preload.js +8 -5
- package/src/renderer/i18n.js +19 -55
- package/src/renderer/index.html +1 -3
- package/src/renderer/renderer.js +336 -155
- package/src/renderer/style.css +1 -60
- package/tests/bg-jobs.test.js +5 -10
- package/tests/llm.test.js +81 -1
- package/tests/python.test.js +3 -2
- package/tests/research.test.js +116 -0
package/src/renderer/style.css
CHANGED
|
@@ -2828,7 +2828,7 @@ body.browser-open #ghOverlay { right: var(--bw, 480px); }
|
|
|
2828
2828
|
width: auto;
|
|
2829
2829
|
text-overflow: ellipsis;
|
|
2830
2830
|
}
|
|
2831
|
-
/* sol bölme sekmeleri: Trend
|
|
2831
|
+
/* sol bölme sekmeleri: Trend Repolar ⇄ Tüm Repolar — eşit genişlik, ortalanmış metin */
|
|
2832
2832
|
.gh-pane-tabs { display: flex; flex: none; }
|
|
2833
2833
|
.gh-tab {
|
|
2834
2834
|
flex: 1 1 0;
|
|
@@ -2852,62 +2852,3 @@ body.browser-open #ghOverlay { right: var(--bw, 480px); }
|
|
|
2852
2852
|
flex: none;
|
|
2853
2853
|
text-align: center;
|
|
2854
2854
|
}
|
|
2855
|
-
|
|
2856
|
-
/* proxy ekleme satırı + liste */
|
|
2857
|
-
.px-add-row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 6px; }
|
|
2858
|
-
.px-add-row select,
|
|
2859
|
-
.px-add-row input {
|
|
2860
|
-
background: var(--panel);
|
|
2861
|
-
border: 1px solid var(--border);
|
|
2862
|
-
border-radius: 7px;
|
|
2863
|
-
color: var(--text);
|
|
2864
|
-
font-size: 12.5px;
|
|
2865
|
-
padding: 6px 9px;
|
|
2866
|
-
outline: none;
|
|
2867
|
-
}
|
|
2868
|
-
.px-add-row select { flex: 0 0 auto; }
|
|
2869
|
-
.px-add-row input:focus { border-color: var(--accent); }
|
|
2870
|
-
#pxHost { flex: 2 1 140px; }
|
|
2871
|
-
#pxPort { flex: 0 1 80px; }
|
|
2872
|
-
#pxUser, #pxPass { flex: 1 1 110px; }
|
|
2873
|
-
.px-row {
|
|
2874
|
-
display: flex;
|
|
2875
|
-
align-items: center;
|
|
2876
|
-
gap: 8px;
|
|
2877
|
-
border: 1px solid var(--border);
|
|
2878
|
-
background: var(--panel);
|
|
2879
|
-
border-radius: 8px;
|
|
2880
|
-
padding: 6px 10px;
|
|
2881
|
-
margin-bottom: 6px;
|
|
2882
|
-
}
|
|
2883
|
-
.px-row code {
|
|
2884
|
-
font-family: var(--mono, monospace);
|
|
2885
|
-
font-size: 12px;
|
|
2886
|
-
color: var(--text);
|
|
2887
|
-
overflow: hidden;
|
|
2888
|
-
text-overflow: ellipsis;
|
|
2889
|
-
white-space: nowrap;
|
|
2890
|
-
flex: 1;
|
|
2891
|
-
}
|
|
2892
|
-
.px-row .px-del {
|
|
2893
|
-
background: none;
|
|
2894
|
-
border: none;
|
|
2895
|
-
color: var(--muted);
|
|
2896
|
-
cursor: pointer;
|
|
2897
|
-
font-size: 13px;
|
|
2898
|
-
padding: 2px 6px;
|
|
2899
|
-
border-radius: 6px;
|
|
2900
|
-
flex: none;
|
|
2901
|
-
}
|
|
2902
|
-
.px-row .px-del:hover { color: var(--err); background: var(--panel2); }
|
|
2903
|
-
|
|
2904
|
-
/* WARP kartı */
|
|
2905
|
-
.warp-card {
|
|
2906
|
-
border: 1px solid var(--border);
|
|
2907
|
-
background: var(--panel);
|
|
2908
|
-
border-radius: 10px;
|
|
2909
|
-
padding: 12px 14px;
|
|
2910
|
-
margin-top: 12px;
|
|
2911
|
-
}
|
|
2912
|
-
.warp-head { display: flex; justify-content: space-between; align-items: center; font-weight: 800; margin-bottom: 8px; }
|
|
2913
|
-
.warp-state { color: var(--muted); font-weight: 400; font-size: 12px; }
|
package/tests/bg-jobs.test.js
CHANGED
|
@@ -121,18 +121,14 @@ function fakeJob(over) {
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
test('superviseReason: takılan stuck,
|
|
124
|
+
test('superviseReason: takılan stuck, uzun koşan iş CEO kontrolü (long), yeni uyarılan null', () => {
|
|
125
125
|
assert.strictEqual(Engine.superviseReason(fakeJob(), Date.now()), 'stuck');
|
|
126
126
|
|
|
127
|
+
// süre sınırı KALDIRILDI: aktivite süren uzun iş artık kesilmez → CEO ara kontrolü (long)
|
|
127
128
|
assert.strictEqual(
|
|
128
129
|
Engine.superviseReason(fakeJob({ lastActivityAt: new Date(Date.now() - 30 * 1000).toISOString() }), Date.now()),
|
|
129
|
-
'wrapup'
|
|
130
|
-
); // aktivite sürüyor ama 10 dk koştu — önce zarif bitirme uyarısı
|
|
131
|
-
|
|
132
|
-
assert.strictEqual(
|
|
133
|
-
Engine.superviseReason(fakeJob({ wrapAt: new Date().toISOString(), lastActivityAt: new Date(Date.now() - 30 * 1000).toISOString() }), Date.now()),
|
|
134
130
|
'long'
|
|
135
|
-
);
|
|
131
|
+
);
|
|
136
132
|
|
|
137
133
|
assert.strictEqual(
|
|
138
134
|
Engine.superviseReason(fakeJob({ startedAt: new Date(Date.now() - 1 * MIN).toISOString() }), Date.now()),
|
|
@@ -182,12 +178,11 @@ test('_supervise önce ajana ÖZ-KURTARMA verir, haklar bitince CEO\'ya uyarır'
|
|
|
182
178
|
eng._supervise();
|
|
183
179
|
assert.strictEqual(job.checks, 1);
|
|
184
180
|
|
|
185
|
-
/* 2. aşama: öz-kurtarma hakları bitmiş + iş hâlâ koşuyor (
|
|
186
|
-
|
|
181
|
+
/* 2. aşama: öz-kurtarma hakları bitmiş + iş hâlâ koşuyor (aktivite sürüyor,
|
|
182
|
+
süre sınırı yok) → CEO uyarısı ('long') */
|
|
187
183
|
job.status = 'running';
|
|
188
184
|
job.endedAt = null;
|
|
189
185
|
job.fixes = 2;
|
|
190
|
-
job.wrapAt = new Date().toISOString();
|
|
191
186
|
job.lastActivityAt = new Date(Date.now() - 30 * 1000).toISOString();
|
|
192
187
|
job.lastNudgeAt = new Date(Date.now() - 5 * MIN).toISOString();
|
|
193
188
|
eng._supervise();
|
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 } = require('../src/agent/llm');
|
|
6
|
+
const { withRetries, friendlyError, sumUsage, chatStreamAuto } = require('../src/agent/llm');
|
|
7
7
|
|
|
8
8
|
test('withRetries: 503 sonrası başarılı isteği tekrarlar', async () => {
|
|
9
9
|
let calls = 0;
|
|
@@ -54,3 +54,83 @@ test('friendlyError ipuçları içerir', () => {
|
|
|
54
54
|
assert.match(m, /kullanılamıyor/);
|
|
55
55
|
assert.match(m, /model seçici/);
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
test('sumUsage: sayısal alanları toplar (devam turu faturalaması)', () => {
|
|
59
|
+
const u = sumUsage(
|
|
60
|
+
{ prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
61
|
+
{ prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 }
|
|
62
|
+
);
|
|
63
|
+
assert.deepEqual(u, { prompt_tokens: 18, completion_tokens: 8, total_tokens: 26 });
|
|
64
|
+
assert.equal(sumUsage(null, { total_tokens: 3 }).total_tokens, 3);
|
|
65
|
+
assert.equal(sumUsage({ total_tokens: 7 }, null).total_tokens, 7);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/* SSE mock sunucusu: finish_reason=length → otomatik devam turu açılır */
|
|
69
|
+
function sseServer(script) {
|
|
70
|
+
const http = require('node:http');
|
|
71
|
+
const calls = [];
|
|
72
|
+
const srv = http.createServer((req, res) => {
|
|
73
|
+
let body = '';
|
|
74
|
+
req.on('data', (c) => (body += c));
|
|
75
|
+
req.on('end', () => {
|
|
76
|
+
calls.push(JSON.parse(body).messages);
|
|
77
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
|
78
|
+
for (const ev of script(calls.length, calls[calls.length - 1])) res.write(ev);
|
|
79
|
+
res.end();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
return { srv, calls };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test('chatStreamAuto: length kesintisinde kaldığı yerden devam eder', async () => {
|
|
86
|
+
const { srv, calls } = sseServer((n) => {
|
|
87
|
+
if (n === 1) {
|
|
88
|
+
return [
|
|
89
|
+
'data: ' + JSON.stringify({ choices: [{ delta: { content: 'Merhaba dun' } }] }) + '\n\n',
|
|
90
|
+
'data: ' + JSON.stringify({ choices: [{ delta: { content: 'ya basladi' }, finish_reason: 'length' }] }) + '\n\n',
|
|
91
|
+
'data: [DONE]\n\n',
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
return [
|
|
95
|
+
'data: ' + JSON.stringify({ choices: [{ delta: { content: 'yor devam' } }] }) + '\n\n',
|
|
96
|
+
'data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\n\n',
|
|
97
|
+
'data: [DONE]\n\n',
|
|
98
|
+
];
|
|
99
|
+
});
|
|
100
|
+
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
|
|
101
|
+
try {
|
|
102
|
+
const port = srv.address().port;
|
|
103
|
+
const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
|
|
104
|
+
const deltas = [];
|
|
105
|
+
const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] }, { onDelta: (d) => deltas.push(d) });
|
|
106
|
+
assert.equal(r.content, 'Merhaba dunya basladiyor devam');
|
|
107
|
+
assert.equal(r.finishReason, 'stop');
|
|
108
|
+
assert.equal(calls.length, 2);
|
|
109
|
+
// 2. turda devam mesajları eklenmiş olmalı (orijinal dizi kopyalanır)
|
|
110
|
+
const m2 = calls[1];
|
|
111
|
+
assert.equal(m2[m2.length - 2].role, 'assistant');
|
|
112
|
+
assert.equal(m2[m2.length - 1].role, 'user');
|
|
113
|
+
assert.match(m2[m2.length - 1].content, /DEVAM/);
|
|
114
|
+
// streaming kesintisiz: 3 delta
|
|
115
|
+
assert.deepEqual(deltas.join(''), r.content);
|
|
116
|
+
} finally {
|
|
117
|
+
srv.close();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('chatStreamAuto: normal (stop) yanıtta hiç devam turu açılmaz', async () => {
|
|
122
|
+
const { srv, calls } = sseServer(() => [
|
|
123
|
+
'data: ' + JSON.stringify({ choices: [{ delta: { content: 'tam cevap' }, finish_reason: 'stop' }] }) + '\n\n',
|
|
124
|
+
'data: [DONE]\n\n',
|
|
125
|
+
]);
|
|
126
|
+
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
|
|
127
|
+
try {
|
|
128
|
+
const port = srv.address().port;
|
|
129
|
+
const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
|
|
130
|
+
const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] });
|
|
131
|
+
assert.equal(r.content, 'tam cevap');
|
|
132
|
+
assert.equal(calls.length, 1);
|
|
133
|
+
} finally {
|
|
134
|
+
srv.close();
|
|
135
|
+
}
|
|
136
|
+
});
|
package/tests/python.test.js
CHANGED
|
@@ -48,8 +48,9 @@ test('paketlenmiş python scriptleri mevcut ve tohumlanır', () => {
|
|
|
48
48
|
/* canlı: python hızlı arama yolu (ağ varsa gerçek sonuç, yoksa JS fallback) */
|
|
49
49
|
test('webSearchFast: python yolu ya da fallback sonuç döner', async function () {
|
|
50
50
|
const r = await tools.webSearchFast('electron builder portable windows', { maxResults: 5 });
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
/* rate-limit/CAPTCHA turlarında motorlar ok:false dönebilir — kablaj testi;
|
|
52
|
+
ağ nazik davranınca sonuç şekli doğrulanır, aksi halde skip */
|
|
53
|
+
if (!r || !r.ok || !r.results.length) return this.skip('motorlar bu turda sonuç döndürmedi (ağ/rate-limit) — kablaj doğru');
|
|
53
54
|
assert.ok(r.results[0].title && r.results[0].url);
|
|
54
55
|
}, { timeout: 40000 });
|
|
55
56
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('./setup');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const assert = require('node:assert');
|
|
6
|
+
|
|
7
|
+
const research = require('../src/agent/research');
|
|
8
|
+
|
|
9
|
+
test('normKey: scheme/www/slash/hash farklarını yok sayar', () => {
|
|
10
|
+
assert.equal(research.normKey('http://www.Example.com/a/'), research.normKey('https://example.com/a#x'));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('mergeResults: round-robin harman + tekilleştirme', () => {
|
|
14
|
+
const a = [
|
|
15
|
+
{ title: 'A1', url: 'https://a.com/1' },
|
|
16
|
+
{ title: 'A2', url: 'https://a.com/2' },
|
|
17
|
+
];
|
|
18
|
+
const b = [
|
|
19
|
+
{ title: 'B1', url: 'https://a.com/1/' }, // A1 ile aynı (slash)
|
|
20
|
+
{ title: 'B1x', url: 'http://www.b.com/x' },
|
|
21
|
+
];
|
|
22
|
+
const merged = research.mergeResults([a, b], 10);
|
|
23
|
+
assert.deepEqual(merged.map((r) => r.url), ['https://a.com/1', 'https://a.com/2', 'http://www.b.com/x']);
|
|
24
|
+
assert.equal(merged[2].title, 'B1x'); // ilk gören kazanır, orijinal url korunur
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('normalizeRows: {results} veya dizi + alternatif anahtarlar', () => {
|
|
28
|
+
const r1 = research.normalizeRows({ results: [{ name: 'X', href: 'https://x.com', body: 'b' }] });
|
|
29
|
+
assert.equal(r1[0].title, 'X');
|
|
30
|
+
assert.equal(r1[0].url, 'https://x.com');
|
|
31
|
+
const r2 = research.normalizeRows([{ title: 'Y', link: 'https://y.com', description: 'd' }]);
|
|
32
|
+
assert.equal(r2[0].url, 'https://y.com');
|
|
33
|
+
assert.equal(research.normalizeRows(null).length, 0);
|
|
34
|
+
assert.equal(research.normalizeRows([{ title: '', url: 'https://z.com' }]).length, 0); // başlıksız elenir
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('deepSearch: paralel sorgular harmanlanır, ilk N sayfa okunur', async () => {
|
|
38
|
+
const searches = {
|
|
39
|
+
'elma': [{ title: 'A', url: 'https://a.com' }, { title: 'B', url: 'https://b.com' }],
|
|
40
|
+
'apple': [{ title: 'B2', url: 'https://b.com' }, { title: 'C', url: 'https://c.com' }],
|
|
41
|
+
};
|
|
42
|
+
const readCalls = [];
|
|
43
|
+
const r = await research.deepSearch(
|
|
44
|
+
{ queries: ['elma', 'apple'], read_top: 2 },
|
|
45
|
+
{
|
|
46
|
+
search: async (q) => searches[q],
|
|
47
|
+
readPage: async (u) => { readCalls.push(u); return { ok: true, url: u, title: 'T:' + u, content: 'icerik ' + u }; },
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
assert.ok(r.ok);
|
|
51
|
+
assert.deepEqual(r.queries, ['elma', 'apple']);
|
|
52
|
+
assert.equal(r.results.length, 3); // b.com duplike düştü
|
|
53
|
+
assert.deepEqual(readCalls, ['https://a.com', 'https://b.com']); // ilk 2 hedef
|
|
54
|
+
assert.equal(r.pages.length, 2);
|
|
55
|
+
assert.ok(r.pages[0].content.includes('https://a.com'));
|
|
56
|
+
assert.match(r.note, /gizli tarayıcı/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('deepSearch: ikili dosya hedefleri okumadan atlanır', async () => {
|
|
60
|
+
const readCalls = [];
|
|
61
|
+
const r = await research.deepSearch(
|
|
62
|
+
{ queries: ['q'], read_top: 3 },
|
|
63
|
+
{
|
|
64
|
+
search: async () => [
|
|
65
|
+
{ title: 'PDF', url: 'https://x.com/doc.pdf' },
|
|
66
|
+
{ title: 'Sayfa', url: 'https://x.com/page' },
|
|
67
|
+
],
|
|
68
|
+
readPage: async (u) => { readCalls.push(u); return { ok: true, url: u, content: 'c' }; },
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
assert.deepEqual(readCalls, ['https://x.com/page']);
|
|
72
|
+
assert.equal(r.pages.length, 1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('deepSearch: readPage yoksa yalnız sonuç listesiyle döner', async () => {
|
|
76
|
+
const r = await research.deepSearch(
|
|
77
|
+
{ queries: ['q'] },
|
|
78
|
+
{ search: async () => [{ title: 'A', url: 'https://a.com' }] }
|
|
79
|
+
);
|
|
80
|
+
assert.ok(r.ok);
|
|
81
|
+
assert.equal(r.results.length, 1);
|
|
82
|
+
assert.equal(r.pages, undefined);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('deepSearch: bir sorgu çökerse diğerleri kurtarır; boşsa ok:false', async () => {
|
|
86
|
+
const r1 = await research.deepSearch(
|
|
87
|
+
{ queries: ['patlar', 'iyi'] },
|
|
88
|
+
{
|
|
89
|
+
search: async (q) => { if (q === 'patlar') throw new Error('boom'); return [{ title: 'A', url: 'https://a.com' }]; },
|
|
90
|
+
readPage: async () => ({ ok: false }),
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
assert.ok(r1.ok && r1.results.length === 1);
|
|
94
|
+
const r2 = await research.deepSearch(
|
|
95
|
+
{ queries: ['yok1', 'yok2'] },
|
|
96
|
+
{ search: async () => [], readPage: async () => ({ ok: false }) }
|
|
97
|
+
);
|
|
98
|
+
assert.equal(r2.ok, false);
|
|
99
|
+
assert.match(r2.error, /boş/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('deepSearch: sorgu yoksa net hata; queries string de kabul edilir', async () => {
|
|
103
|
+
const bad = await research.deepSearch({}, { search: async () => [] });
|
|
104
|
+
assert.equal(bad.ok, false);
|
|
105
|
+
const r = await research.deepSearch(
|
|
106
|
+
{ queries: 'tek sorgu' },
|
|
107
|
+
{ search: async () => [{ title: 'A', url: 'https://a.com' }] }
|
|
108
|
+
);
|
|
109
|
+
assert.deepEqual(r.queries, ['tek sorgu']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('SKIP_FILE_RE: pdf/görsel/arşiv yakalanır, sayfa yakalanmaz', () => {
|
|
113
|
+
assert.ok(research.SKIP_FILE_RE.test('https://x.com/a.pdf'));
|
|
114
|
+
assert.ok(research.SKIP_FILE_RE.test('https://x.com/img.png?w=9'));
|
|
115
|
+
assert.ok(!research.SKIP_FILE_RE.test('https://x.com/article'));
|
|
116
|
+
});
|