beast-agent 0.30.0 → 1.0.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 +6 -3
- package/src/agent/engine.js +463 -21
- package/src/agent/llm.js +52 -3
- package/src/agent/obscura.js +26 -10
- package/src/agent/skills.js +2 -2
- package/src/agent/tools.js +519 -7
- package/src/main.js +28 -13
- package/src/renderer/i18n.js +7 -7
- package/src/renderer/index.html +5 -2
- package/src/renderer/renderer.js +154 -5
- package/src/renderer/style.css +45 -4
- package/.env.example +0 -8
- package/config.example.yaml +0 -39
- package/eng.traineddata +0 -0
- package/scripts/fix-electron.js +0 -126
- package/scripts/release.js +0 -121
- package/scripts/swap-electron.js +0 -40
- package/store/skills.json +0 -5
- package/tests/approval.test.js +0 -56
- package/tests/bg-jobs.test.js +0 -424
- package/tests/bots-name.test.js +0 -42
- package/tests/bus.test.js +0 -46
- package/tests/computeruse-context.test.js +0 -69
- package/tests/cron.test.js +0 -95
- package/tests/engine.test.js +0 -203
- package/tests/eval.test.js +0 -91
- package/tests/fallout.test.js +0 -188
- package/tests/llm.test.js +0 -205
- package/tests/memory.test.js +0 -33
- package/tests/memoryloop.test.js +0 -26
- package/tests/notegen.test.js +0 -63
- package/tests/obscura.test.js +0 -124
- package/tests/owner.test.js +0 -32
- package/tests/python.test.js +0 -76
- package/tests/research.test.js +0 -116
- package/tests/scenarios.json +0 -81
- package/tests/sessioncode.test.js +0 -50
- package/tests/setup.js +0 -9
- package/tests/tokens.test.js +0 -42
- package/tests/tools.test.js +0 -84
- package/tests/usage.test.js +0 -44
- package/tests/v11.test.js +0 -76
- package/tests/watchers.test.js +0 -206
- package/tests/watext.test.js +0 -53
- package/tests/whatsapp.test.js +0 -103
- package/tests/wherewasi.test.js +0 -40
package/src/agent/tools.js
CHANGED
|
@@ -22,6 +22,284 @@ function truncateMiddle(s, cap) {
|
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/* ---------- opencode port: truncate.ts ----------
|
|
26
|
+
MAX_LINES 2000 / MAX_BYTES 50KB aşarsa TAM çıktı geçici dosyaya yazılır;
|
|
27
|
+
modele sınırlı önizleme + dosya yolu döner (Model Tool Output bounding).
|
|
28
|
+
Böylece hiçbir araç çıktısı KAYBOLMAZ — ajan read_file ile kalanını okur. */
|
|
29
|
+
const TRUNC_MAX_LINES = 2000;
|
|
30
|
+
const TRUNC_MAX_BYTES = 50 * 1024;
|
|
31
|
+
const TRUNC_PREVIEW_CHARS = 6000; /* engine'in 7200'lik dilimi içinde ipucu kalsın */
|
|
32
|
+
|
|
33
|
+
function toolOutputDir() {
|
|
34
|
+
const d = path.join(os.tmpdir(), 'beast-tool-output');
|
|
35
|
+
try { fs.mkdirSync(d, { recursive: true }); } catch {}
|
|
36
|
+
return d;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function boundToolOutput(text) {
|
|
40
|
+
const s = String(text || '');
|
|
41
|
+
const bytes = Buffer.byteLength(s, 'utf8');
|
|
42
|
+
const lines = s.split('\n').length;
|
|
43
|
+
if (bytes <= TRUNC_MAX_BYTES && lines <= TRUNC_MAX_LINES) return { text: s };
|
|
44
|
+
const file = path.join(
|
|
45
|
+
toolOutputDir(),
|
|
46
|
+
`tool_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}.txt`
|
|
47
|
+
);
|
|
48
|
+
try {
|
|
49
|
+
fs.writeFileSync(file, s);
|
|
50
|
+
} catch {
|
|
51
|
+
return { text: s.slice(0, TRUNC_PREVIEW_CHARS) + '\n…[kırpıldı]' };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
text:
|
|
55
|
+
s.slice(0, TRUNC_PREVIEW_CHARS) +
|
|
56
|
+
`\n\n[çıktı kırpıldı: ${lines} satır / ${bytes} byte — TAM ÇIKTI: ${file}\nread_file'ı offset/limit ile kullanarak kalan bölümleri oku]`,
|
|
57
|
+
outputFile: file,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ---------- opencode port: shell.ts + shell/prompt.ts ----------
|
|
62
|
+
Workspace başına KALICI PowerShell oturumu: cd/env çağrılar arasında
|
|
63
|
+
korunur, her çağrıda process spawn maliyeti yoktur. Komut bitişi benzersiz
|
|
64
|
+
işaretle algılanır (prompt senkronizasyonu). Paralel çağrılar oturum
|
|
65
|
+
başına sıraya girer (opencode ile aynı: oturum başına sıralı yürütme). */
|
|
66
|
+
const SHELL_QUEUE_CAP = 6; /* en fazla bu kadar ayrı workspace oturumu */
|
|
67
|
+
const SHELL_IDLE_MS = 10 * 60 * 1000; /* 10 dk boşta kalan oturum kapatılır */
|
|
68
|
+
const _shSessions = new Map(); // cwd → session
|
|
69
|
+
|
|
70
|
+
/* boşta reaper: unref'li zamanlayıcı — event loop'u tek başına tutmaz */
|
|
71
|
+
const _shellReaper = setInterval(() => {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
for (const [k, sess] of _shSessions) {
|
|
74
|
+
if (!sess.busy && now - (sess.lastUsed || 0) > SHELL_IDLE_MS) {
|
|
75
|
+
_shellDispose(sess);
|
|
76
|
+
_shSessions.delete(k);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, 60000);
|
|
80
|
+
if (_shellReaper.unref) _shellReaper.unref();
|
|
81
|
+
|
|
82
|
+
function _shellDispose(sess) {
|
|
83
|
+
try {
|
|
84
|
+
if (sess.proc && sess.proc.pid) {
|
|
85
|
+
spawn('taskkill', ['/pid', String(sess.proc.pid), '/T', '/F'], { windowsHide: true });
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
try { sess.proc && sess.proc.kill(); } catch {}
|
|
89
|
+
sess.dead = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function disposeShellSessions() {
|
|
93
|
+
for (const sess of _shSessions.values()) _shellDispose(sess);
|
|
94
|
+
_shSessions.clear();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _spawnShell(cwd) {
|
|
98
|
+
const proc = spawn(
|
|
99
|
+
'powershell.exe',
|
|
100
|
+
['-NoProfile', '-NoExit', '-Command', '-'],
|
|
101
|
+
{ cwd: cwd || process.cwd(), windowsHide: true, env: envWithPathPrefix() }
|
|
102
|
+
);
|
|
103
|
+
const sess = {
|
|
104
|
+
proc,
|
|
105
|
+
cwd,
|
|
106
|
+
buf: '',
|
|
107
|
+
err: '',
|
|
108
|
+
seq: 0,
|
|
109
|
+
busy: false,
|
|
110
|
+
queue: [], // {command, resolve, timer, signal, onAbort}
|
|
111
|
+
dead: false,
|
|
112
|
+
};
|
|
113
|
+
/* Node 22: child + stdio stream'leri event loop'a bağlanmaz — test/CLI'da
|
|
114
|
+
bekleyen oturum sürecin çıkmasını ENGELLEMEZ; uygulama kapanışında
|
|
115
|
+
disposeShellSessions() yine de temiz kapatır */
|
|
116
|
+
try {
|
|
117
|
+
proc.unref && proc.unref();
|
|
118
|
+
proc.stdin.unref && proc.stdin.unref();
|
|
119
|
+
proc.stdout.unref && proc.stdout.unref();
|
|
120
|
+
proc.stderr.unref && proc.stderr.unref();
|
|
121
|
+
} catch {}
|
|
122
|
+
proc.stdin.on && proc.stdin.on('error', () => {}); // EPIPE yut — oturum ölünce yazma patlamasın
|
|
123
|
+
proc.stdout.setEncoding('utf8');
|
|
124
|
+
proc.stderr.setEncoding('utf8');
|
|
125
|
+
proc.stdout.on('data', (d) => {
|
|
126
|
+
sess.buf += d;
|
|
127
|
+
_shellPump(sess);
|
|
128
|
+
});
|
|
129
|
+
proc.stderr.on('data', (d) => {
|
|
130
|
+
sess.err += d;
|
|
131
|
+
if (sess.err.length > MAX_CMD_OUTPUT * 4) sess.err = sess.err.slice(-MAX_CMD_OUTPUT * 2);
|
|
132
|
+
});
|
|
133
|
+
proc.on('exit', () => {
|
|
134
|
+
sess.dead = true;
|
|
135
|
+
/* bekleyen komutları ölü oturumda düşür — yeni çağrı taze oturum açar */
|
|
136
|
+
while (sess.queue.length) {
|
|
137
|
+
const w = sess.queue.shift();
|
|
138
|
+
clearTimeout(w.timer);
|
|
139
|
+
w.resolve({ ok: false, code: null, output: sess.buf.slice(-2000) + '\n[beast] shell oturumu kapandı' });
|
|
140
|
+
}
|
|
141
|
+
sess.busy = false;
|
|
142
|
+
if (_shSessions.get(cwd) === sess) _shSessions.delete(cwd);
|
|
143
|
+
});
|
|
144
|
+
proc.on('error', () => {
|
|
145
|
+
sess.dead = true;
|
|
146
|
+
});
|
|
147
|
+
return sess;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function _shellPump(sess) {
|
|
151
|
+
const w = sess.queue[0];
|
|
152
|
+
if (!w || !w.dispatched) return;
|
|
153
|
+
const marker = `${SHELL_MARKER_PREFIX}${w.n}_`;
|
|
154
|
+
const mi = sess.buf.indexOf(marker);
|
|
155
|
+
if (mi < 0) return;
|
|
156
|
+
const lineEnd = sess.buf.indexOf('\n', mi);
|
|
157
|
+
if (lineEnd < 0) return; /* işaret satırı tam gelmedi — daha fazla veri bekle */
|
|
158
|
+
const markerLine = sess.buf.slice(mi, lineEnd).trim();
|
|
159
|
+
let out = sess.buf.slice(0, mi);
|
|
160
|
+
sess.buf = sess.buf.slice(lineEnd + 1);
|
|
161
|
+
sess.queue.shift();
|
|
162
|
+
sess.busy = sess.queue.length > 0;
|
|
163
|
+
clearTimeout(w.timer);
|
|
164
|
+
const codeM = /_(\-?\d+)\s*$/.exec(markerLine);
|
|
165
|
+
const exitCode = codeM ? Number(codeM[1]) : null;
|
|
166
|
+
if (out.length > MAX_CMD_OUTPUT * 4) out = out.slice(-MAX_CMD_OUTPUT * 2);
|
|
167
|
+
const errPart = sess.err.trim();
|
|
168
|
+
sess.err = '';
|
|
169
|
+
w.resolve({
|
|
170
|
+
ok: exitCode === 0,
|
|
171
|
+
code: exitCode,
|
|
172
|
+
output:
|
|
173
|
+
(out.trim() || '') +
|
|
174
|
+
(errPart ? (out.trim() ? '\n[stderr] ' : '') + errPart : ''),
|
|
175
|
+
});
|
|
176
|
+
sess.lastUsed = Date.now();
|
|
177
|
+
if (sess.queue.length) _shellDispatch(sess);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const SHELL_MARKER_PREFIX = '__BEAST_DONE_';
|
|
181
|
+
|
|
182
|
+
function _shellDispatch(sess) {
|
|
183
|
+
const w = sess.queue[0];
|
|
184
|
+
if (!w || w.dispatched) return;
|
|
185
|
+
w.dispatched = true;
|
|
186
|
+
w.n = ++sess.seq;
|
|
187
|
+
const cmd = w.command;
|
|
188
|
+
const marker = `${SHELL_MARKER_PREFIX}${w.n}_$LASTEXITCODE`;
|
|
189
|
+
/* $LASTEXITCODE sıfırlanır: yalnız cmdlet koşan komutlar da ok:true dönsün;
|
|
190
|
+
native exe hata verirse gerçek kod işaret satırına yazılır */
|
|
191
|
+
sess.proc.stdin.write(`$global:LASTEXITCODE = 0\n${cmd}${cmd.endsWith('\n') ? '' : '\n'}Write-Output "${marker}"\n`);
|
|
192
|
+
const finishTimeout = () => {
|
|
193
|
+
_shellDispose(sess);
|
|
194
|
+
if (_shSessions.get(sess.cwd) === sess) _shSessions.delete(sess.cwd);
|
|
195
|
+
sess.busy = false;
|
|
196
|
+
while (sess.queue.length) {
|
|
197
|
+
const x = sess.queue.shift();
|
|
198
|
+
clearTimeout(x.timer);
|
|
199
|
+
x.resolve({ ok: false, code: null, output: '[beast] shell komutu zaman aşımı — oturum tazelendi' });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
w.timer = setTimeout(finishTimeout, w.timeoutMs);
|
|
203
|
+
if (w.signal) {
|
|
204
|
+
if (w.signal.aborted) return finishTimeout();
|
|
205
|
+
w.signal.addEventListener('abort', finishTimeout, { once: true });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function runShellCommand(command, cwd, timeoutMs = 90000, signal) {
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
let sess = _shSessions.get(cwd);
|
|
212
|
+
if (!sess || sess.dead) {
|
|
213
|
+
try {
|
|
214
|
+
sess = _spawnShell(cwd);
|
|
215
|
+
_shSessions.set(cwd, sess);
|
|
216
|
+
while (_shSessions.size > SHELL_QUEUE_CAP) {
|
|
217
|
+
const [k, old] = _shSessions.entries().next().value;
|
|
218
|
+
if (k === cwd) break;
|
|
219
|
+
_shellDispose(old);
|
|
220
|
+
_shSessions.delete(k);
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
/* kalıcı oturum açılamadı → tek seferlik klasik yol */
|
|
224
|
+
return runCommand(command, cwd, timeoutMs, signal).then(resolve);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
sess.queue.push({ command, resolve, timeoutMs, signal, dispatched: false });
|
|
228
|
+
sess.lastUsed = Date.now();
|
|
229
|
+
if (!sess.busy) {
|
|
230
|
+
sess.busy = true;
|
|
231
|
+
_shellDispatch(sess);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* --- core/shell.ts gitbash() port: Windows'ta bash çözümleme sırası --- */
|
|
237
|
+
function gitbash() {
|
|
238
|
+
const cands = [];
|
|
239
|
+
if (process.env.ProgramFiles) cands.push(path.join(process.env.ProgramFiles, 'Git', 'bin', 'bash.exe'));
|
|
240
|
+
if (process.env['ProgramFiles(x86)']) cands.push(path.join(process.env['ProgramFiles(x86)'], 'Git', 'bin', 'bash.exe'));
|
|
241
|
+
if (process.env.LOCALAPPDATA) cands.push(path.join(process.env.LOCALAPPDATA, 'Programs', 'Git', 'bin', 'bash.exe'));
|
|
242
|
+
for (const c of cands) {
|
|
243
|
+
try { if (fs.existsSync(c)) return c; } catch {}
|
|
244
|
+
}
|
|
245
|
+
return 'bash'; /* PATH'te aranır; yoksa spawn ENOENT → net hata mesajı */
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function runBashCommand(command, cwd, timeoutMs = 90000, signal) {
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
let settled = false;
|
|
251
|
+
let out = '';
|
|
252
|
+
let err = '';
|
|
253
|
+
const finish = (code, killed) => {
|
|
254
|
+
if (settled) return;
|
|
255
|
+
settled = true;
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
const text = (out ? out.trim() : '') + (err ? (out ? '\n[stderr] ' : '') + err.trim() : '');
|
|
258
|
+
resolve({ ok: !killed && code === 0, code, output: truncateMiddle(text || '(no output)', MAX_CMD_OUTPUT) });
|
|
259
|
+
};
|
|
260
|
+
let child;
|
|
261
|
+
try {
|
|
262
|
+
child = spawn(gitbash(), ['-lc', command], {
|
|
263
|
+
cwd: cwd || process.cwd(),
|
|
264
|
+
windowsHide: true,
|
|
265
|
+
env: envWithPathPrefix(),
|
|
266
|
+
});
|
|
267
|
+
} catch {
|
|
268
|
+
resolve({ ok: false, code: null, output: 'bash bulunamadı — Git for Windows kur ya da PowerShell kullan' });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const timer = setTimeout(() => {
|
|
272
|
+
try { spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true }); } catch {}
|
|
273
|
+
err += '\n[beast] command timed out';
|
|
274
|
+
finish(null, true);
|
|
275
|
+
}, timeoutMs);
|
|
276
|
+
if (signal) {
|
|
277
|
+
if (signal.aborted) return finish(null, true);
|
|
278
|
+
signal.addEventListener('abort', () => {
|
|
279
|
+
try { spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true }); } catch {}
|
|
280
|
+
finish(null, true);
|
|
281
|
+
}, { once: true });
|
|
282
|
+
}
|
|
283
|
+
child.stdout.setEncoding('utf8');
|
|
284
|
+
child.stderr.setEncoding('utf8');
|
|
285
|
+
child.stdout.on('data', (d) => { out += d; });
|
|
286
|
+
child.stderr.on('data', (d) => { err += d; });
|
|
287
|
+
child.on('error', (e) => {
|
|
288
|
+
clearTimeout(timer);
|
|
289
|
+
settled = true;
|
|
290
|
+
const msg = String((e && e.message) || '');
|
|
291
|
+
resolve({
|
|
292
|
+
ok: false,
|
|
293
|
+
code: null,
|
|
294
|
+
output: /ENOENT/i.test(msg)
|
|
295
|
+
? 'bash bulunamadı — Git for Windows kur ya da PowerShell kullan'
|
|
296
|
+
: msg,
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
child.on('close', (code) => finish(code, false));
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
25
303
|
function runCommand(command, cwd, timeoutMs = 90000, signal) {
|
|
26
304
|
return new Promise((resolve) => {
|
|
27
305
|
let settled = false;
|
|
@@ -86,6 +364,59 @@ function safeResolve(p, cwd) {
|
|
|
86
364
|
return path.normalize(abs);
|
|
87
365
|
}
|
|
88
366
|
|
|
367
|
+
/* ---------- opencode port: grep/glob altyapısı ----------
|
|
368
|
+
ripgrep gitignore'u izler; Beast'te bağımlılık olmasın diye ağır klasörler
|
|
369
|
+
statik atlanır (node_modules, .git, derleme çıktıları…). */
|
|
370
|
+
const SKIP_DIRS = new Set([
|
|
371
|
+
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'env',
|
|
372
|
+
'dist', 'build', 'out', 'coverage', '.next', '.turbo', '.cache',
|
|
373
|
+
]);
|
|
374
|
+
|
|
375
|
+
/* mini glob → regex: ** → her şey, * → / içermeyen her şey, ? → tek karakter */
|
|
376
|
+
function globToRegExp(glob) {
|
|
377
|
+
const g = String(glob || '').trim();
|
|
378
|
+
if (!g) return null;
|
|
379
|
+
let re = '';
|
|
380
|
+
for (let i = 0; i < g.length; i++) {
|
|
381
|
+
const c = g[i];
|
|
382
|
+
if (c === '*') {
|
|
383
|
+
if (g[i + 1] === '*') {
|
|
384
|
+
re += '.*';
|
|
385
|
+
i++;
|
|
386
|
+
if (g[i + 1] === '/') i++; /* yıldız-yıldız-slash: üst klasörler opsiyonel */
|
|
387
|
+
} else re += '[^/\\\\]*';
|
|
388
|
+
} else if (c === '?') re += '[^/\\\\]';
|
|
389
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
return new RegExp('^' + re + '$', 'i');
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* Sıralı dosya yürüyüşü; cb true dönerse erken durur */
|
|
399
|
+
function walkFiles(dir, includeRe, cb, depth = 0) {
|
|
400
|
+
if (depth > 24) return false;
|
|
401
|
+
let entries = [];
|
|
402
|
+
try {
|
|
403
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
404
|
+
} catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
for (const e of entries) {
|
|
408
|
+
const full = path.join(dir, e.name);
|
|
409
|
+
if (e.isDirectory()) {
|
|
410
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.') && e.name !== '.opencode') continue;
|
|
411
|
+
if (walkFiles(full, includeRe, cb, depth + 1)) return true;
|
|
412
|
+
} else if (e.isFile()) {
|
|
413
|
+
if (includeRe && !includeRe.test(e.name)) continue;
|
|
414
|
+
if (cb(full)) return true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
|
|
89
420
|
/* ---------- Python altyapısı (#18) ----------
|
|
90
421
|
Sıra: BEAST_PYTHON env → sistem python/python3/py -3 → gömülü runtime
|
|
91
422
|
(%APPDATA%\beast\py — yoksa TEK SEFERLİK kendisi indirip açar).
|
|
@@ -598,7 +929,7 @@ const definitions = [
|
|
|
598
929
|
type: 'function',
|
|
599
930
|
function: {
|
|
600
931
|
name: 'write_file',
|
|
601
|
-
description: 'Write (or overwrite) a text file with content. Creates parent directories.',
|
|
932
|
+
description: 'Write (or overwrite) a text file with content. Creates parent directories. PREFER edit_file for changing existing files — write_file is for NEW files or full rewrites only.',
|
|
602
933
|
parameters: {
|
|
603
934
|
type: 'object',
|
|
604
935
|
properties: {
|
|
@@ -609,6 +940,57 @@ const definitions = [
|
|
|
609
940
|
},
|
|
610
941
|
},
|
|
611
942
|
},
|
|
943
|
+
{
|
|
944
|
+
type: 'function',
|
|
945
|
+
function: {
|
|
946
|
+
name: 'edit_file',
|
|
947
|
+
description:
|
|
948
|
+
'Performs exact string replacements in files. Usage: read the file first; pass oldString (exact text including indentation, WITHOUT line-number prefixes) and newString. FAILS if oldString is not found ("oldString not found in content") or found multiple times ("Found multiple matches...") — provide more surrounding lines to make it unique, or set replace_all:true to replace every instance. ALWAYS prefer this tool over write_file for existing files; NEVER reformat unrelated code.',
|
|
949
|
+
parameters: {
|
|
950
|
+
type: 'object',
|
|
951
|
+
properties: {
|
|
952
|
+
path: { type: 'string' },
|
|
953
|
+
old_string: { type: 'string', description: 'Exact text to replace (must match file content verbatim)' },
|
|
954
|
+
new_string: { type: 'string', description: 'Replacement text (can be empty to delete)' },
|
|
955
|
+
replace_all: { type: 'boolean', description: 'Replace every occurrence instead of failing on multiple matches' },
|
|
956
|
+
},
|
|
957
|
+
required: ['path', 'old_string', 'new_string'],
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
type: 'function',
|
|
963
|
+
function: {
|
|
964
|
+
name: 'grep',
|
|
965
|
+
description:
|
|
966
|
+
'Fast content search tool that works with any codebase size. Searches file contents using regular expressions; supports full regex syntax (eg. "log.*Error", "function\\s+\\w+"). Filter files by pattern with include (eg. "*.js", "*.{ts,tsx}"). Returns file paths, line numbers and matching lines. node_modules/.git and similar dirs are skipped. Use this to find where functions/symbols/errors live before editing.',
|
|
967
|
+
parameters: {
|
|
968
|
+
type: 'object',
|
|
969
|
+
properties: {
|
|
970
|
+
pattern: { type: 'string', description: 'Regular expression' },
|
|
971
|
+
path: { type: 'string', description: 'File or directory to search; defaults to workspace root' },
|
|
972
|
+
include: { type: 'string', description: 'Glob filter like "*.js" or "*.{ts,tsx}"' },
|
|
973
|
+
},
|
|
974
|
+
required: ['pattern'],
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
type: 'function',
|
|
980
|
+
function: {
|
|
981
|
+
name: 'glob',
|
|
982
|
+
description:
|
|
983
|
+
'Fast file pattern matching tool that works with any codebase size. Supports glob patterns like "**/*.js" or "src/**/*.ts". Returns matching file paths. Use when you need to find files by name patterns; batch multiple speculative searches in one turn.',
|
|
984
|
+
parameters: {
|
|
985
|
+
type: 'object',
|
|
986
|
+
properties: {
|
|
987
|
+
pattern: { type: 'string', description: 'Glob pattern, e.g. "src/**/*.ts"' },
|
|
988
|
+
path: { type: 'string', description: 'Directory to search; defaults to workspace root' },
|
|
989
|
+
},
|
|
990
|
+
required: ['pattern'],
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
},
|
|
612
994
|
{
|
|
613
995
|
type: 'function',
|
|
614
996
|
function: {
|
|
@@ -706,8 +1088,46 @@ async function exec(name, args, ctx) {
|
|
|
706
1088
|
switch (name) {
|
|
707
1089
|
case 'run_command': {
|
|
708
1090
|
const t = Number(args.timeout_ms) || 90000;
|
|
709
|
-
const
|
|
710
|
-
|
|
1091
|
+
const shell = String(args.shell || 'powershell').toLowerCase();
|
|
1092
|
+
const r =
|
|
1093
|
+
shell === 'bash' || shell === 'sh'
|
|
1094
|
+
? await runBashCommand(String(args.command || ''), cwd, t, ctx.signal)
|
|
1095
|
+
: await runShellCommand(String(args.command || ''), cwd, t, ctx.signal);
|
|
1096
|
+
const b = boundToolOutput((r && r.output) || '');
|
|
1097
|
+
return JSON.stringify({
|
|
1098
|
+
ok: !!(r && r.ok),
|
|
1099
|
+
code: r && r.code,
|
|
1100
|
+
output: b.text,
|
|
1101
|
+
...(b.outputFile ? { outputFile: b.outputFile } : {}),
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
case 'edit_file': {
|
|
1105
|
+
const abs = safeResolve(String(args.path || ''), cwd);
|
|
1106
|
+
const oldS = String(args.old_string ?? args.oldString ?? '');
|
|
1107
|
+
const newS = String(args.new_string ?? args.newString ?? '');
|
|
1108
|
+
if (!oldS) return JSON.stringify({ ok: false, error: 'old_string boş olamaz' });
|
|
1109
|
+
if (!fs.existsSync(abs)) return JSON.stringify({ ok: false, error: 'dosya bulunamadı: ' + abs });
|
|
1110
|
+
const src = fs.readFileSync(abs, 'utf8');
|
|
1111
|
+
const count = src.split(oldS).length - 1;
|
|
1112
|
+
if (count === 0) {
|
|
1113
|
+
return JSON.stringify({
|
|
1114
|
+
ok: false,
|
|
1115
|
+
error: 'oldString not found in content — dosyayı read_file ile tekrar oku; satır numarası ön ekini (ör. "1: ") EKLEME, içerik birebir olmalı',
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
if (count > 1 && !args.replace_all && !args.replaceAll) {
|
|
1119
|
+
return JSON.stringify({
|
|
1120
|
+
ok: false,
|
|
1121
|
+
error: `Found multiple matches for oldString (${count} kez). Daha fazla bağlam satırı ekleyerek benzersiz yap ya da replace_all:true ver`,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
const out = args.replace_all || args.replaceAll ? src.split(oldS).join(newS) : src.replace(oldS, newS);
|
|
1125
|
+
fs.writeFileSync(abs, out, 'utf8');
|
|
1126
|
+
return JSON.stringify({
|
|
1127
|
+
ok: true,
|
|
1128
|
+
path: abs,
|
|
1129
|
+
replacements: args.replace_all || args.replaceAll ? count : 1,
|
|
1130
|
+
});
|
|
711
1131
|
}
|
|
712
1132
|
case 'read_file': {
|
|
713
1133
|
const abs = safeResolve(String(args.path || ''), cwd);
|
|
@@ -732,10 +1152,40 @@ async function exec(name, args, ctx) {
|
|
|
732
1152
|
return JSON.stringify({ ok: false, error: 'pdf okuma hata: ' + String((e2 && e2.message) || e2) });
|
|
733
1153
|
}
|
|
734
1154
|
}
|
|
735
|
-
let
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1155
|
+
let raw = fs.readFileSync(abs, 'utf8');
|
|
1156
|
+
/* opencode read.ts port: satır numaralı çıktı (`N: içerik`), offset/limit
|
|
1157
|
+
penceresi (1-indexed), 2000 satır varsayılan, uzun satır kırpma */
|
|
1158
|
+
const allLines = raw.split('\n');
|
|
1159
|
+
const offset = Math.max(1, Math.floor(Number(args.offset) || 1));
|
|
1160
|
+
const limit = Math.min(2000, Math.max(1, Math.floor(Number(args.limit) || 2000)));
|
|
1161
|
+
let slice = allLines
|
|
1162
|
+
.slice(offset - 1, offset - 1 + limit)
|
|
1163
|
+
.map((l, i) => {
|
|
1164
|
+
const line = l.length > 2000 ? l.slice(0, 2000) + '…[satır kırpıldı]' : l;
|
|
1165
|
+
return `${offset + i}: ${line}`;
|
|
1166
|
+
});
|
|
1167
|
+
let truncated = offset - 1 + limit < allLines.length;
|
|
1168
|
+
let content = slice.join('\n');
|
|
1169
|
+
/* byte tavanı: 200k karakteri aşan pencereler satır bazında kırpılır */
|
|
1170
|
+
if (content.length > MAX_FILE_CHARS) {
|
|
1171
|
+
const keep = [];
|
|
1172
|
+
let used = 0;
|
|
1173
|
+
for (const l of slice) {
|
|
1174
|
+
if (used + l.length > MAX_FILE_CHARS) break;
|
|
1175
|
+
keep.push(l);
|
|
1176
|
+
used += l.length + 1;
|
|
1177
|
+
}
|
|
1178
|
+
content = keep.join('\n');
|
|
1179
|
+
truncated = true;
|
|
1180
|
+
}
|
|
1181
|
+
return JSON.stringify({
|
|
1182
|
+
ok: true,
|
|
1183
|
+
path: abs,
|
|
1184
|
+
totalLines: allLines.length,
|
|
1185
|
+
offset,
|
|
1186
|
+
truncated,
|
|
1187
|
+
content,
|
|
1188
|
+
});
|
|
739
1189
|
}
|
|
740
1190
|
case 'write_file': {
|
|
741
1191
|
const abs = safeResolve(String(args.path || ''), cwd);
|
|
@@ -756,6 +1206,61 @@ async function exec(name, args, ctx) {
|
|
|
756
1206
|
});
|
|
757
1207
|
return JSON.stringify({ ok: true, path: abs, count: rows.length, entries: rows.join('\n') });
|
|
758
1208
|
}
|
|
1209
|
+
case 'grep': {
|
|
1210
|
+
const pattern = String(args.pattern || '');
|
|
1211
|
+
let re;
|
|
1212
|
+
try {
|
|
1213
|
+
re = new RegExp(pattern, 'i');
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
return JSON.stringify({ ok: false, error: 'geçersiz regex: ' + String((e && e.message) || e) });
|
|
1216
|
+
}
|
|
1217
|
+
const root = args.path ? safeResolve(String(args.path), cwd) : cwd;
|
|
1218
|
+
if (!fs.existsSync(root)) return JSON.stringify({ ok: false, error: 'yol bulunamadı: ' + root });
|
|
1219
|
+
const incRe = globToRegExp(String(args.include || ''));
|
|
1220
|
+
const matches = [];
|
|
1221
|
+
const MAX_MATCHES = 200;
|
|
1222
|
+
walkFiles(root, incRe, (file) => {
|
|
1223
|
+
if (matches.length >= MAX_MATCHES) return true; // dur
|
|
1224
|
+
let text = '';
|
|
1225
|
+
try {
|
|
1226
|
+
const st = fs.statSync(file);
|
|
1227
|
+
if (st.size > 2 * 1024 * 1024) return false; // dev dosyayı atla
|
|
1228
|
+
text = fs.readFileSync(file, 'utf8');
|
|
1229
|
+
} catch {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
const lines = text.split('\n');
|
|
1233
|
+
for (let i = 0; i < lines.length && matches.length < MAX_MATCHES; i++) {
|
|
1234
|
+
if (re.test(lines[i])) {
|
|
1235
|
+
matches.push(`${file}:${i + 1}: ${lines[i].trim().slice(0, 300)}`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return false;
|
|
1239
|
+
});
|
|
1240
|
+
return JSON.stringify({
|
|
1241
|
+
ok: true,
|
|
1242
|
+
pattern,
|
|
1243
|
+
count: matches.length,
|
|
1244
|
+
capped: matches.length >= MAX_MATCHES,
|
|
1245
|
+
matches: matches.join('\n'),
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
case 'glob': {
|
|
1249
|
+
const pat = String(args.pattern || '');
|
|
1250
|
+
if (!pat.trim()) return JSON.stringify({ ok: false, error: 'pattern boş olamaz' });
|
|
1251
|
+
const root = args.path ? safeResolve(String(args.path), cwd) : cwd;
|
|
1252
|
+
if (!fs.existsSync(root)) return JSON.stringify({ ok: false, error: 'yol bulunamadı: ' + root });
|
|
1253
|
+
const re = globToRegExp(pat);
|
|
1254
|
+
const out = [];
|
|
1255
|
+
walkFiles(root, null, (file) => {
|
|
1256
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
1257
|
+
if (re.test(rel) || re.test(path.basename(file))) {
|
|
1258
|
+
out.push(path.join(root, rel));
|
|
1259
|
+
}
|
|
1260
|
+
return out.length >= 500; // dur
|
|
1261
|
+
});
|
|
1262
|
+
return JSON.stringify({ ok: true, pattern: pat, count: out.length, files: out });
|
|
1263
|
+
}
|
|
759
1264
|
case 'web_search': {
|
|
760
1265
|
const q = String(args.query || '');
|
|
761
1266
|
const n = Number(args.max_results) || 8;
|
|
@@ -973,6 +1478,13 @@ module.exports = {
|
|
|
973
1478
|
definitions,
|
|
974
1479
|
exec,
|
|
975
1480
|
runCommand,
|
|
1481
|
+
runShellCommand,
|
|
1482
|
+
runBashCommand,
|
|
1483
|
+
gitbash,
|
|
1484
|
+
disposeShellSessions,
|
|
1485
|
+
boundToolOutput,
|
|
1486
|
+
globToRegExp,
|
|
1487
|
+
walkFiles,
|
|
976
1488
|
assertPublicHttpUrl,
|
|
977
1489
|
parseDdgResults,
|
|
978
1490
|
htmlToText,
|
package/src/main.js
CHANGED
|
@@ -153,6 +153,9 @@ function startNpmUpdateWatch() {
|
|
|
153
153
|
const toolsMod = require('./agent/tools');
|
|
154
154
|
const { htmlToText, setSearchChain, setSearchObscuraEnabled, setTinyfishKey } = toolsMod;
|
|
155
155
|
const obscura = require('./agent/obscura');
|
|
156
|
+
/* OpenCode köprüsü KALDIRILDI: Beast Code artık tamamen BEAST motoruyla
|
|
157
|
+
çalışır — opencode'in döngü mantığı (compaction, prune, cache disiplini,
|
|
158
|
+
doom-loop, yetim onarım) engine.js'e native port edildi. */
|
|
156
159
|
const { waToolLine } = require('./agent/watext');
|
|
157
160
|
|
|
158
161
|
/* #3 merkezî log sistemine process-seviye hataları da düşsün */
|
|
@@ -223,7 +226,7 @@ ensureBeastCode();
|
|
|
223
226
|
startHealthServer(); /* splash/boot aşamasından itibaren /health ayakta */
|
|
224
227
|
try { setSearchObscuraEnabled(settings.obscuraEnabled !== false); } catch {} /* Obscura varsayılan AKTİF */
|
|
225
228
|
try { setSearchChain(settings.searchChain); } catch {}
|
|
226
|
-
try { setTinyfishKey(settings.tinyfishKey || null); } catch {}
|
|
229
|
+
try { setTinyfishKey(settings.tinyfishKey || null); } catch {}
|
|
227
230
|
/* kurulumda Obscura da kurulsun: yoksa ARKA PLANDA otomatik indir (UI kilitlenmez);
|
|
228
231
|
ilerleme Ayarlar → Web Arama'dan da izlenebilir */
|
|
229
232
|
setTimeout(() => {
|
|
@@ -2758,6 +2761,7 @@ app.whenReady().then(() => {
|
|
|
2758
2761
|
app.on('before-quit', () => {
|
|
2759
2762
|
app.isQuitting = true;
|
|
2760
2763
|
flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
|
|
2764
|
+
try { toolsMod.disposeShellSessions(); } catch {} // kalıcı shell oturumlarını kapat
|
|
2761
2765
|
});
|
|
2762
2766
|
|
|
2763
2767
|
if (process.argv.includes('--smoke')) {
|
|
@@ -5211,25 +5215,35 @@ function bcGetSession(folder) {
|
|
|
5211
5215
|
return s;
|
|
5212
5216
|
}
|
|
5213
5217
|
|
|
5214
|
-
|
|
5218
|
+
/* Beast Code motoru: TAMAMEN BEAST MOTORU — opencode'in döngü mantığı
|
|
5219
|
+
(compaction, prune, prompt-cache, doom-loop, yetim onarım, proje
|
|
5220
|
+
talimatları) engine.js'e native port edildi; köprü/alt süreç yok. */
|
|
5221
|
+
|
|
5222
|
+
function bcPanelEvent(sid, ev) {
|
|
5223
|
+
try {
|
|
5224
|
+
if (win && !win.isDestroyed()) win.webContents.send('agent:event', { sessionId: sid, ...ev });
|
|
5225
|
+
} catch {}
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
ipcMain.handle('beastcode:send', async (_e, payload) => {
|
|
5215
5229
|
const text = String((payload && payload.msg) || '').trim();
|
|
5216
5230
|
if (!text) return { ok: false, error: 'boş mesaj' };
|
|
5217
5231
|
if (!engine) return { ok: false, error: 'ajan hazır değil' };
|
|
5218
|
-
if (!engine.publicState().hasModel) return { ok: false, error: 'model yok — Ayarlar → Provider sekmesinden ekle' };
|
|
5219
5232
|
const ws = ideRoot();
|
|
5233
|
+
const modeM = /^\/(plan|build|auto)\b/i.exec(text);
|
|
5234
|
+
|
|
5235
|
+
if (!engine.publicState().hasModel) return { ok: false, error: 'model yok — Ayarlar → Provider sekmesinden ekle' };
|
|
5220
5236
|
if (engine.isBusy(bcSessions.get(ws))) return { ok: false, busy: true, error: 'önceki mesaj sürüyor — ■ ile durdurabilirsin' };
|
|
5221
5237
|
const s = bcGetSession(ws);
|
|
5222
5238
|
s.workspace = ws; /* soldaki klasörde çalış */
|
|
5223
5239
|
s.bcCode = true; /* todo disiplini + iş sonu hızlı kapanış (engine) */
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
if (m) {
|
|
5227
|
-
s.bcMode = m[1].toLowerCase();
|
|
5240
|
+
if (modeM) {
|
|
5241
|
+
s.bcMode = modeM[1].toLowerCase();
|
|
5228
5242
|
engine.cache.set(s.id, s);
|
|
5229
5243
|
if (win && !win.isDestroyed()) {
|
|
5230
|
-
const body =
|
|
5244
|
+
const body = modeM[1].toLowerCase() === 'plan'
|
|
5231
5245
|
? 'PLAN MODU — dosyaları okuyup inceler, KOD YAZMAZ; adım adım uygulama planı verir.'
|
|
5232
|
-
:
|
|
5246
|
+
: modeM[1].toLowerCase() === 'build'
|
|
5233
5247
|
? 'BUILD MODU — son planı UYGULAR: dosyaları düzenler, komutları çalıştırır, doğrular.'
|
|
5234
5248
|
: 'OTOMATİK MOD — önce kısa plan, sonra uygulama + doğrulama (OpenCode disiplini).';
|
|
5235
5249
|
win.webContents.send('agent:event', { sessionId: s.id, type: 'bc-mode', mode: s.bcMode, body });
|
|
@@ -5242,15 +5256,16 @@ ipcMain.handle('beastcode:send', (_e, payload) => {
|
|
|
5242
5256
|
return { ok: true, sessionId: s.id };
|
|
5243
5257
|
});
|
|
5244
5258
|
|
|
5245
|
-
ipcMain.handle('beastcode:stop', () => {
|
|
5246
|
-
const
|
|
5259
|
+
ipcMain.handle('beastcode:stop', async () => {
|
|
5260
|
+
const ws = ideRoot();
|
|
5261
|
+
const sid = bcSessions.get(ws);
|
|
5247
5262
|
if (!sid) return { ok: false };
|
|
5248
5263
|
let r = false;
|
|
5249
5264
|
try { r = engine.interrupt(sid, 'kullanıcı Beast Code panelinden ■ ile durdurdu'); } catch {}
|
|
5250
5265
|
return { ok: !!r };
|
|
5251
5266
|
});
|
|
5252
5267
|
|
|
5253
|
-
ipcMain.handle('beastcode:new', () => {
|
|
5268
|
+
ipcMain.handle('beastcode:new', async () => {
|
|
5254
5269
|
const ws = ideRoot();
|
|
5255
5270
|
const sid = bcSessions.get(ws);
|
|
5256
5271
|
if (sid && engine.isBusy(sid)) return { ok: false, error: 'mesaj sürüyor — önce ■ ile durdur' };
|
|
@@ -5259,7 +5274,7 @@ ipcMain.handle('beastcode:new', () => {
|
|
|
5259
5274
|
bcSessions.delete(ws);
|
|
5260
5275
|
}
|
|
5261
5276
|
return { ok: true };
|
|
5262
|
-
});
|
|
5277
|
+
});
|
|
5263
5278
|
|
|
5264
5279
|
/* düşünme (reasoning) seviyesi */
|
|
5265
5280
|
ipcMain.handle('think:set', (_e, v) => {
|