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.
Files changed (46) hide show
  1. package/package.json +6 -3
  2. package/src/agent/engine.js +463 -21
  3. package/src/agent/llm.js +52 -3
  4. package/src/agent/obscura.js +26 -10
  5. package/src/agent/skills.js +2 -2
  6. package/src/agent/tools.js +519 -7
  7. package/src/main.js +28 -13
  8. package/src/renderer/i18n.js +7 -7
  9. package/src/renderer/index.html +5 -2
  10. package/src/renderer/renderer.js +154 -5
  11. package/src/renderer/style.css +45 -4
  12. package/.env.example +0 -8
  13. package/config.example.yaml +0 -39
  14. package/eng.traineddata +0 -0
  15. package/scripts/fix-electron.js +0 -126
  16. package/scripts/release.js +0 -121
  17. package/scripts/swap-electron.js +0 -40
  18. package/store/skills.json +0 -5
  19. package/tests/approval.test.js +0 -56
  20. package/tests/bg-jobs.test.js +0 -424
  21. package/tests/bots-name.test.js +0 -42
  22. package/tests/bus.test.js +0 -46
  23. package/tests/computeruse-context.test.js +0 -69
  24. package/tests/cron.test.js +0 -95
  25. package/tests/engine.test.js +0 -203
  26. package/tests/eval.test.js +0 -91
  27. package/tests/fallout.test.js +0 -188
  28. package/tests/llm.test.js +0 -205
  29. package/tests/memory.test.js +0 -33
  30. package/tests/memoryloop.test.js +0 -26
  31. package/tests/notegen.test.js +0 -63
  32. package/tests/obscura.test.js +0 -124
  33. package/tests/owner.test.js +0 -32
  34. package/tests/python.test.js +0 -76
  35. package/tests/research.test.js +0 -116
  36. package/tests/scenarios.json +0 -81
  37. package/tests/sessioncode.test.js +0 -50
  38. package/tests/setup.js +0 -9
  39. package/tests/tokens.test.js +0 -42
  40. package/tests/tools.test.js +0 -84
  41. package/tests/usage.test.js +0 -44
  42. package/tests/v11.test.js +0 -76
  43. package/tests/watchers.test.js +0 -206
  44. package/tests/watext.test.js +0 -53
  45. package/tests/whatsapp.test.js +0 -103
  46. package/tests/wherewasi.test.js +0 -40
@@ -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 r = await runCommand(String(args.command || ''), cwd, t, ctx.signal);
710
- return JSON.stringify(r);
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 content = fs.readFileSync(abs, 'utf8');
736
- const truncated = content.length > MAX_FILE_CHARS;
737
- if (truncated) content = content.slice(0, MAX_FILE_CHARS);
738
- return JSON.stringify({ ok: true, path: abs, truncated, content });
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
- ipcMain.handle('beastcode:send', (_e, payload) => {
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
- /* OpenCode disiplini — çalışma modu: /plan (incele+planla) · /build (uygula) · /auto */
5225
- const m = /^\/(plan|build|auto)\b/i.exec(text);
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 = m[1].toLowerCase() === 'plan'
5244
+ const body = modeM[1].toLowerCase() === 'plan'
5231
5245
  ? 'PLAN MODU — dosyaları okuyup inceler, KOD YAZMAZ; adım adım uygulama planı verir.'
5232
- : m[1].toLowerCase() === 'build'
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 sid = bcSessions.get(ideRoot());
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) => {