beast-agent 1.7.0 → 1.8.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/bin/beast-agent.js +12 -0
- package/package.json +1 -1
- package/src/agent/engine.js +7 -7
- package/src/agent/llm.js +46 -34
- package/src/agent/scripts/stealthsearch.py +30 -0
- package/src/agent/searxng.js +325 -0
- package/src/agent/seeds/brainstorming/SKILL.md +90 -0
- package/src/agent/seeds/dispatching-parallel-agents/SKILL.md +120 -0
- package/src/agent/seeds/executing-plans/SKILL.md +60 -0
- package/src/agent/seeds/subagent-driven-development/SKILL.md +167 -0
- package/src/agent/seeds/systematic-debugging/SKILL.md +131 -0
- package/src/agent/seeds/test-driven-development/SKILL.md +152 -0
- package/src/agent/seeds/verification-before-completion/SKILL.md +63 -0
- package/src/agent/seeds/writing-plans/SKILL.md +162 -0
- package/src/agent/seeds/writing-skills/SKILL.md +229 -0
- package/src/agent/skills.js +39 -9
- package/src/agent/tools.js +2340 -2273
- package/src/main.js +7048 -7106
- package/src/preload.js +188 -192
- package/src/renderer/i18n.js +1186 -1226
- package/src/renderer/index.html +0 -6
- package/src/renderer/renderer.js +7389 -7480
- package/src/renderer/style.css +3383 -3430
- package/src/agent/obscura.js +0 -292
package/src/agent/tools.js
CHANGED
|
@@ -1,2273 +1,2340 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const https = require('https');
|
|
7
|
-
const { execFile } = require('child_process');
|
|
8
|
-
const { spawn } = require('child_process');
|
|
9
|
-
const research = require('./research');
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const MAX_CMD_OUTPUT = 16000;
|
|
13
|
-
const MAX_FILE_CHARS = 200000;
|
|
14
|
-
|
|
15
|
-
function truncateMiddle(s, cap) {
|
|
16
|
-
if (s.length <= cap) return s;
|
|
17
|
-
const half = Math.floor((cap - 32) / 2);
|
|
18
|
-
return (
|
|
19
|
-
s.slice(0, half) +
|
|
20
|
-
`\n... [${s.length - cap} chars truncated] ...\n` +
|
|
21
|
-
s.slice(s.length - half)
|
|
22
|
-
);
|
|
23
|
-
}
|
|
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
|
-
|
|
303
|
-
function runCommand(command, cwd, timeoutMs = 90000, signal) {
|
|
304
|
-
return new Promise((resolve) => {
|
|
305
|
-
let settled = false;
|
|
306
|
-
let out = '';
|
|
307
|
-
let err = '';
|
|
308
|
-
const finish = (code, killed) => {
|
|
309
|
-
if (settled) return;
|
|
310
|
-
settled = true;
|
|
311
|
-
clearTimeout(timer);
|
|
312
|
-
const text =
|
|
313
|
-
(out ? out.trim() : '') +
|
|
314
|
-
(err ? (out ? '\n[stderr] ' : '') + err.trim() : '');
|
|
315
|
-
resolve({
|
|
316
|
-
ok: !killed && code === 0,
|
|
317
|
-
output: truncateMiddle(text || '(no output)', MAX_CMD_OUTPUT),
|
|
318
|
-
code,
|
|
319
|
-
});
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
const child = spawn(
|
|
323
|
-
'powershell.exe',
|
|
324
|
-
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command],
|
|
325
|
-
{ cwd: cwd || process.cwd(), windowsHide: true, env: envWithPathPrefix() }
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
const timer = setTimeout(() => {
|
|
329
|
-
try {
|
|
330
|
-
spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
|
|
331
|
-
} catch {}
|
|
332
|
-
finish(null, true);
|
|
333
|
-
// append notice
|
|
334
|
-
err += '\n[beast] command timed out';
|
|
335
|
-
}, timeoutMs);
|
|
336
|
-
|
|
337
|
-
if (signal) {
|
|
338
|
-
if (signal.aborted) return finish(null, true);
|
|
339
|
-
signal.addEventListener('abort', () => {
|
|
340
|
-
try {
|
|
341
|
-
spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
|
|
342
|
-
} catch {}
|
|
343
|
-
finish(null, true);
|
|
344
|
-
}, { once: true });
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
child.stdout.setEncoding('utf8');
|
|
348
|
-
child.stderr.setEncoding('utf8');
|
|
349
|
-
child.stdout.on('data', (d) => {
|
|
350
|
-
out += d;
|
|
351
|
-
if (out.length > MAX_CMD_OUTPUT * 4) out = out.slice(-MAX_CMD_OUTPUT * 2);
|
|
352
|
-
});
|
|
353
|
-
child.stderr.on('data', (d) => {
|
|
354
|
-
err += d;
|
|
355
|
-
if (err.length > MAX_CMD_OUTPUT * 4) err = err.slice(-MAX_CMD_OUTPUT * 2);
|
|
356
|
-
});
|
|
357
|
-
child.on('error', (e) => finish(1, false) || void (err += String(e.message)));
|
|
358
|
-
child.on('close', (code) => finish(code, false));
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function safeResolve(p, cwd) {
|
|
363
|
-
const abs = path.isAbsolute(p) ? p : path.join(cwd || process.cwd(), p);
|
|
364
|
-
return path.normalize(abs);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/* ---------- opencode read.ts port: binary tespit + fuzzy öneri ----------
|
|
368
|
-
opencode read.ts kuralı: uzantı kara listesi YA DA ilk 4KB'ta NUL byte YA DA
|
|
369
|
-
%30+'ı yazdırılamaz karakter → dosya binary sayılır, okunmaz. */
|
|
370
|
-
const BINARY_EXT_RE =
|
|
371
|
-
/\.(zip|tar|gz|tgz|bz2|xz|7z|rar|exe|dll|so|dylib|bin|iso|img|msi|apk|jar|class|pyc|pyo|o|obj|a|lib|woff2?|ttf|otf|eot|mp3|mp4|avi|mkv|mov|flac|ogg|wav|webm|psd|ai|sketch|db|sqlite3?|pdb|docx?|xlsx?|pptx?|odt|ods|odp|pgp|gpg|keystore|jks|p12|traineddata|idx)$/i;
|
|
372
|
-
|
|
373
|
-
function looksBinary(buf) {
|
|
374
|
-
const sample = buf.length > 4096 ? buf.subarray(0, 4096) : buf;
|
|
375
|
-
if (sample.includes(0)) return true;
|
|
376
|
-
let nonPrintable = 0;
|
|
377
|
-
for (const b of sample) {
|
|
378
|
-
if (b === 9 || b === 10 || b === 13) continue; // \t \n \r
|
|
379
|
-
if (b < 32 || b === 127 || b >= 0x80) nonPrintable++; // 0x80+ UTF-8 devam byte'ı olabilir ama kaba tarama yeterli
|
|
380
|
-
}
|
|
381
|
-
return sample.length > 0 && nonPrintable / sample.length > 0.3;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/* opencode read.ts: dosya yoksa aynı klasörde isim ön-ek benzerliği olan 3 kardeş öner */
|
|
385
|
-
function fuzzySiblings(abs) {
|
|
386
|
-
try {
|
|
387
|
-
const dir = path.dirname(abs);
|
|
388
|
-
const base = path.basename(abs).toLowerCase();
|
|
389
|
-
const prefix = base.slice(0, 4);
|
|
390
|
-
const score = (name) => {
|
|
391
|
-
const n = name.toLowerCase();
|
|
392
|
-
if (n === base) return -1;
|
|
393
|
-
let s = 0;
|
|
394
|
-
if (n.startsWith(prefix)) s += 2;
|
|
395
|
-
for (let i = 0; i < Math.min(base.length, n.length); i++) if (base[i] === n[i]) s += 0.1;
|
|
396
|
-
return s;
|
|
397
|
-
};
|
|
398
|
-
return fs
|
|
399
|
-
.readdirSync(dir)
|
|
400
|
-
.map((name) => ({ name, s: score(name) }))
|
|
401
|
-
.filter((x) => x.s > 0)
|
|
402
|
-
.sort((a, b) => b.s - a.s)
|
|
403
|
-
.slice(0, 3)
|
|
404
|
-
.map((x) => x.name);
|
|
405
|
-
} catch {
|
|
406
|
-
return [];
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/* ---------- opencode port: grep/glob altyapısı ----------
|
|
411
|
-
ripgrep gitignore'u izler; Beast'te bağımlılık olmasın diye ağır klasörler
|
|
412
|
-
statik atlanır (node_modules, .git, derleme çıktıları…). */
|
|
413
|
-
const SKIP_DIRS = new Set([
|
|
414
|
-
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'env',
|
|
415
|
-
'dist', 'build', 'out', 'coverage', '.next', '.turbo', '.cache',
|
|
416
|
-
]);
|
|
417
|
-
|
|
418
|
-
/* mini glob → regex: ** → her şey, * → / içermeyen her şey, ? → tek karakter */
|
|
419
|
-
function globToRegExp(glob) {
|
|
420
|
-
const g = String(glob || '').trim();
|
|
421
|
-
if (!g) return null;
|
|
422
|
-
let re = '';
|
|
423
|
-
for (let i = 0; i < g.length; i++) {
|
|
424
|
-
const c = g[i];
|
|
425
|
-
if (c === '*') {
|
|
426
|
-
if (g[i + 1] === '*') {
|
|
427
|
-
re += '.*';
|
|
428
|
-
i++;
|
|
429
|
-
if (g[i + 1] === '/') i++; /* yıldız-yıldız-slash: üst klasörler opsiyonel */
|
|
430
|
-
} else re += '[^/\\\\]*';
|
|
431
|
-
} else if (c === '?') re += '[^/\\\\]';
|
|
432
|
-
else re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
433
|
-
}
|
|
434
|
-
try {
|
|
435
|
-
return new RegExp('^' + re + '$', 'i');
|
|
436
|
-
} catch {
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/* Sıralı dosya yürüyüşü; cb true dönerse erken durur */
|
|
442
|
-
function walkFiles(dir, includeRe, cb, depth = 0) {
|
|
443
|
-
if (depth > 24) return false;
|
|
444
|
-
let entries = [];
|
|
445
|
-
try {
|
|
446
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
447
|
-
} catch {
|
|
448
|
-
return false;
|
|
449
|
-
}
|
|
450
|
-
for (const e of entries) {
|
|
451
|
-
const full = path.join(dir, e.name);
|
|
452
|
-
if (e.isDirectory()) {
|
|
453
|
-
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.') && e.name !== '.opencode') continue;
|
|
454
|
-
if (walkFiles(full, includeRe, cb, depth + 1)) return true;
|
|
455
|
-
} else if (e.isFile()) {
|
|
456
|
-
if (includeRe && !includeRe.test(e.name)) continue;
|
|
457
|
-
if (cb(full)) return true;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
return false;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/* ---------- Python altyapısı (#18) ----------
|
|
464
|
-
Sıra: BEAST_PYTHON env → sistem python/python3/py -3 → gömülü runtime
|
|
465
|
-
(%APPDATA%\beast\py — yoksa TEK SEFERLİK kendisi indirip açar).
|
|
466
|
-
Böylece ajanlar makinede Python kurulmasa bile python_run kullanabilir. */
|
|
467
|
-
|
|
468
|
-
const PYTHON_EMBED_URL = 'https://www.python.org/ftp/python/3.12.8/python-3.12.8-embed-amd64.zip';
|
|
469
|
-
const PY_PROBE_TIMEOUT = 6000;
|
|
470
|
-
|
|
471
|
-
function beastAppDir() {
|
|
472
|
-
const base = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
473
|
-
return path.join(base, 'beast');
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function pythonScriptsDir() {
|
|
477
|
-
const d = path.join(beastAppDir(), 'scripts');
|
|
478
|
-
fs.mkdirSync(d, { recursive: true });
|
|
479
|
-
return d;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function embeddedPythonExe() {
|
|
483
|
-
return path.join(beastAppDir(), 'py', 'python.exe');
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/* Gömülü runtime kuruluysa klasörünü PATH önüne koy: ajanın run_command'ında
|
|
487
|
-
`python` / `python.exe` da çalışsın (makinede Python kurulmuş olmasa bile). */
|
|
488
|
-
function pythonPathPrefix() {
|
|
489
|
-
try {
|
|
490
|
-
const d = path.dirname(embeddedPythonExe());
|
|
491
|
-
if (fs.existsSync(path.join(d, 'python.exe'))) return d + path.delimiter;
|
|
492
|
-
} catch {}
|
|
493
|
-
return '';
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function envWithPathPrefix() {
|
|
497
|
-
const base = { ...process.env };
|
|
498
|
-
base.PATH = pythonPathPrefix() + (process.env.PATH || process.env.Path || '');
|
|
499
|
-
return base;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/* paketlenmiş python scriptleri (src/agent/scripts) → kullanıcı scripts klasörüne tohumla */
|
|
503
|
-
function bundledScriptPath(name) {
|
|
504
|
-
return path.join(__dirname, 'scripts', name);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function seedScript(name, force = false) {
|
|
508
|
-
const src = bundledScriptPath(name);
|
|
509
|
-
try {
|
|
510
|
-
const dest = path.join(pythonScriptsDir(), name);
|
|
511
|
-
/* force: paket içindeki güncel sürüm her zaman kazanır (sistem scripti) */
|
|
512
|
-
if (fs.existsSync(src) && (force || !fs.existsSync(dest))) {
|
|
513
|
-
fs.copyFileSync(src, dest);
|
|
514
|
-
}
|
|
515
|
-
return dest;
|
|
516
|
-
} catch {
|
|
517
|
-
return path.join(pythonScriptsDir(), name);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/* #20 hızlı web arama: ddgs kütüphanesi varsa onu kullan; yoksa python çoklu-motor
|
|
522
|
-
paralel; o da olmazsa JS fallback. Sonuç boş gelirse (CAPTCHA/engel) ddgs'yi
|
|
523
|
-
TEK SEFERLİK arka planda kurup tekrar dener. */
|
|
524
|
-
function pyExec(exe, args, timeoutMs) {
|
|
525
|
-
return new Promise((resolve) => {
|
|
526
|
-
try {
|
|
527
|
-
execFile(exe, args, { timeout: timeoutMs, windowsHide: true }, (err, stdout, stderr) =>
|
|
528
|
-
resolve({ err, out: String(stdout || '') + String(stderr || '') })
|
|
529
|
-
);
|
|
530
|
-
} catch (e) {
|
|
531
|
-
resolve({ err: e, out: '' });
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
let _ddgsTried = null; // Promise<boolean> — oturum başına tek kurulum denemesi
|
|
537
|
-
|
|
538
|
-
function ddgsImportOk(probe) {
|
|
539
|
-
/* hızlı yerel kontrol: kütüphane import edilebiliyor mu (ağ gerektirmez) */
|
|
540
|
-
return pyExec(probe.exe, ['-c', 'import ddgs'], 15000).then((r) => !r.err);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/* pip ile TEK SEFERLİK kurulum dener; uzun sürebilir — await ETME, arka planda bırak */
|
|
544
|
-
function ensureDdgs(probe) {
|
|
545
|
-
if (_ddgsTried) return _ddgsTried;
|
|
546
|
-
_ddgsTried = (async () => {
|
|
547
|
-
try {
|
|
548
|
-
if (await ddgsImportOk(probe)) return true;
|
|
549
|
-
const pip = await pyExec(probe.exe, ['-m', 'pip', '--version'], 15000);
|
|
550
|
-
if (pip.err) return false; // gömülü python'da pip yok — sessizce vazgeç
|
|
551
|
-
const inst = await pyExec(
|
|
552
|
-
probe.exe,
|
|
553
|
-
['-m', 'pip', 'install', '--quiet', '--disable-pip-version-check', 'ddgs'],
|
|
554
|
-
120000
|
|
555
|
-
);
|
|
556
|
-
if (inst.err) return false;
|
|
557
|
-
return await ddgsImportOk(probe);
|
|
558
|
-
} catch {
|
|
559
|
-
return false;
|
|
560
|
-
}
|
|
561
|
-
})();
|
|
562
|
-
return _ddgsTried;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function parseWsOutput(output) {
|
|
566
|
-
return String(output || '')
|
|
567
|
-
.split('\n')
|
|
568
|
-
.map((l) => l.trim())
|
|
569
|
-
.filter((l) => l.startsWith('{'))
|
|
570
|
-
.map((l) => {
|
|
571
|
-
try { return JSON.parse(l); } catch { return null; }
|
|
572
|
-
})
|
|
573
|
-
.filter(Boolean)
|
|
574
|
-
.map((x) => ({ title: String(x.title || ''), url: String(x.url || ''), snippet: String(x.snippet || ''), engine: String(x.engine || '') }));
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
let
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
let
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
{
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
'
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
path: { type: 'string', description: '
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
'
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
]
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
return JSON.stringify(
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
path:
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
.
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
});
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
)
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
}
|
|
2029
|
-
if (
|
|
2030
|
-
return JSON.stringify({
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
return
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
case '
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
if (
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const { execFile } = require('child_process');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const research = require('./research');
|
|
10
|
+
const searxng = require('./searxng');
|
|
11
|
+
|
|
12
|
+
const MAX_CMD_OUTPUT = 16000;
|
|
13
|
+
const MAX_FILE_CHARS = 200000;
|
|
14
|
+
|
|
15
|
+
function truncateMiddle(s, cap) {
|
|
16
|
+
if (s.length <= cap) return s;
|
|
17
|
+
const half = Math.floor((cap - 32) / 2);
|
|
18
|
+
return (
|
|
19
|
+
s.slice(0, half) +
|
|
20
|
+
`\n... [${s.length - cap} chars truncated] ...\n` +
|
|
21
|
+
s.slice(s.length - half)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
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
|
+
|
|
303
|
+
function runCommand(command, cwd, timeoutMs = 90000, signal) {
|
|
304
|
+
return new Promise((resolve) => {
|
|
305
|
+
let settled = false;
|
|
306
|
+
let out = '';
|
|
307
|
+
let err = '';
|
|
308
|
+
const finish = (code, killed) => {
|
|
309
|
+
if (settled) return;
|
|
310
|
+
settled = true;
|
|
311
|
+
clearTimeout(timer);
|
|
312
|
+
const text =
|
|
313
|
+
(out ? out.trim() : '') +
|
|
314
|
+
(err ? (out ? '\n[stderr] ' : '') + err.trim() : '');
|
|
315
|
+
resolve({
|
|
316
|
+
ok: !killed && code === 0,
|
|
317
|
+
output: truncateMiddle(text || '(no output)', MAX_CMD_OUTPUT),
|
|
318
|
+
code,
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const child = spawn(
|
|
323
|
+
'powershell.exe',
|
|
324
|
+
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command],
|
|
325
|
+
{ cwd: cwd || process.cwd(), windowsHide: true, env: envWithPathPrefix() }
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const timer = setTimeout(() => {
|
|
329
|
+
try {
|
|
330
|
+
spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
|
|
331
|
+
} catch {}
|
|
332
|
+
finish(null, true);
|
|
333
|
+
// append notice
|
|
334
|
+
err += '\n[beast] command timed out';
|
|
335
|
+
}, timeoutMs);
|
|
336
|
+
|
|
337
|
+
if (signal) {
|
|
338
|
+
if (signal.aborted) return finish(null, true);
|
|
339
|
+
signal.addEventListener('abort', () => {
|
|
340
|
+
try {
|
|
341
|
+
spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
|
|
342
|
+
} catch {}
|
|
343
|
+
finish(null, true);
|
|
344
|
+
}, { once: true });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
child.stdout.setEncoding('utf8');
|
|
348
|
+
child.stderr.setEncoding('utf8');
|
|
349
|
+
child.stdout.on('data', (d) => {
|
|
350
|
+
out += d;
|
|
351
|
+
if (out.length > MAX_CMD_OUTPUT * 4) out = out.slice(-MAX_CMD_OUTPUT * 2);
|
|
352
|
+
});
|
|
353
|
+
child.stderr.on('data', (d) => {
|
|
354
|
+
err += d;
|
|
355
|
+
if (err.length > MAX_CMD_OUTPUT * 4) err = err.slice(-MAX_CMD_OUTPUT * 2);
|
|
356
|
+
});
|
|
357
|
+
child.on('error', (e) => finish(1, false) || void (err += String(e.message)));
|
|
358
|
+
child.on('close', (code) => finish(code, false));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function safeResolve(p, cwd) {
|
|
363
|
+
const abs = path.isAbsolute(p) ? p : path.join(cwd || process.cwd(), p);
|
|
364
|
+
return path.normalize(abs);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* ---------- opencode read.ts port: binary tespit + fuzzy öneri ----------
|
|
368
|
+
opencode read.ts kuralı: uzantı kara listesi YA DA ilk 4KB'ta NUL byte YA DA
|
|
369
|
+
%30+'ı yazdırılamaz karakter → dosya binary sayılır, okunmaz. */
|
|
370
|
+
const BINARY_EXT_RE =
|
|
371
|
+
/\.(zip|tar|gz|tgz|bz2|xz|7z|rar|exe|dll|so|dylib|bin|iso|img|msi|apk|jar|class|pyc|pyo|o|obj|a|lib|woff2?|ttf|otf|eot|mp3|mp4|avi|mkv|mov|flac|ogg|wav|webm|psd|ai|sketch|db|sqlite3?|pdb|docx?|xlsx?|pptx?|odt|ods|odp|pgp|gpg|keystore|jks|p12|traineddata|idx)$/i;
|
|
372
|
+
|
|
373
|
+
function looksBinary(buf) {
|
|
374
|
+
const sample = buf.length > 4096 ? buf.subarray(0, 4096) : buf;
|
|
375
|
+
if (sample.includes(0)) return true;
|
|
376
|
+
let nonPrintable = 0;
|
|
377
|
+
for (const b of sample) {
|
|
378
|
+
if (b === 9 || b === 10 || b === 13) continue; // \t \n \r
|
|
379
|
+
if (b < 32 || b === 127 || b >= 0x80) nonPrintable++; // 0x80+ UTF-8 devam byte'ı olabilir ama kaba tarama yeterli
|
|
380
|
+
}
|
|
381
|
+
return sample.length > 0 && nonPrintable / sample.length > 0.3;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* opencode read.ts: dosya yoksa aynı klasörde isim ön-ek benzerliği olan 3 kardeş öner */
|
|
385
|
+
function fuzzySiblings(abs) {
|
|
386
|
+
try {
|
|
387
|
+
const dir = path.dirname(abs);
|
|
388
|
+
const base = path.basename(abs).toLowerCase();
|
|
389
|
+
const prefix = base.slice(0, 4);
|
|
390
|
+
const score = (name) => {
|
|
391
|
+
const n = name.toLowerCase();
|
|
392
|
+
if (n === base) return -1;
|
|
393
|
+
let s = 0;
|
|
394
|
+
if (n.startsWith(prefix)) s += 2;
|
|
395
|
+
for (let i = 0; i < Math.min(base.length, n.length); i++) if (base[i] === n[i]) s += 0.1;
|
|
396
|
+
return s;
|
|
397
|
+
};
|
|
398
|
+
return fs
|
|
399
|
+
.readdirSync(dir)
|
|
400
|
+
.map((name) => ({ name, s: score(name) }))
|
|
401
|
+
.filter((x) => x.s > 0)
|
|
402
|
+
.sort((a, b) => b.s - a.s)
|
|
403
|
+
.slice(0, 3)
|
|
404
|
+
.map((x) => x.name);
|
|
405
|
+
} catch {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* ---------- opencode port: grep/glob altyapısı ----------
|
|
411
|
+
ripgrep gitignore'u izler; Beast'te bağımlılık olmasın diye ağır klasörler
|
|
412
|
+
statik atlanır (node_modules, .git, derleme çıktıları…). */
|
|
413
|
+
const SKIP_DIRS = new Set([
|
|
414
|
+
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'env',
|
|
415
|
+
'dist', 'build', 'out', 'coverage', '.next', '.turbo', '.cache',
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
/* mini glob → regex: ** → her şey, * → / içermeyen her şey, ? → tek karakter */
|
|
419
|
+
function globToRegExp(glob) {
|
|
420
|
+
const g = String(glob || '').trim();
|
|
421
|
+
if (!g) return null;
|
|
422
|
+
let re = '';
|
|
423
|
+
for (let i = 0; i < g.length; i++) {
|
|
424
|
+
const c = g[i];
|
|
425
|
+
if (c === '*') {
|
|
426
|
+
if (g[i + 1] === '*') {
|
|
427
|
+
re += '.*';
|
|
428
|
+
i++;
|
|
429
|
+
if (g[i + 1] === '/') i++; /* yıldız-yıldız-slash: üst klasörler opsiyonel */
|
|
430
|
+
} else re += '[^/\\\\]*';
|
|
431
|
+
} else if (c === '?') re += '[^/\\\\]';
|
|
432
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
return new RegExp('^' + re + '$', 'i');
|
|
436
|
+
} catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* Sıralı dosya yürüyüşü; cb true dönerse erken durur */
|
|
442
|
+
function walkFiles(dir, includeRe, cb, depth = 0) {
|
|
443
|
+
if (depth > 24) return false;
|
|
444
|
+
let entries = [];
|
|
445
|
+
try {
|
|
446
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
for (const e of entries) {
|
|
451
|
+
const full = path.join(dir, e.name);
|
|
452
|
+
if (e.isDirectory()) {
|
|
453
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.') && e.name !== '.opencode') continue;
|
|
454
|
+
if (walkFiles(full, includeRe, cb, depth + 1)) return true;
|
|
455
|
+
} else if (e.isFile()) {
|
|
456
|
+
if (includeRe && !includeRe.test(e.name)) continue;
|
|
457
|
+
if (cb(full)) return true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* ---------- Python altyapısı (#18) ----------
|
|
464
|
+
Sıra: BEAST_PYTHON env → sistem python/python3/py -3 → gömülü runtime
|
|
465
|
+
(%APPDATA%\beast\py — yoksa TEK SEFERLİK kendisi indirip açar).
|
|
466
|
+
Böylece ajanlar makinede Python kurulmasa bile python_run kullanabilir. */
|
|
467
|
+
|
|
468
|
+
const PYTHON_EMBED_URL = 'https://www.python.org/ftp/python/3.12.8/python-3.12.8-embed-amd64.zip';
|
|
469
|
+
const PY_PROBE_TIMEOUT = 6000;
|
|
470
|
+
|
|
471
|
+
function beastAppDir() {
|
|
472
|
+
const base = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
473
|
+
return path.join(base, 'beast');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function pythonScriptsDir() {
|
|
477
|
+
const d = path.join(beastAppDir(), 'scripts');
|
|
478
|
+
fs.mkdirSync(d, { recursive: true });
|
|
479
|
+
return d;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function embeddedPythonExe() {
|
|
483
|
+
return path.join(beastAppDir(), 'py', 'python.exe');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* Gömülü runtime kuruluysa klasörünü PATH önüne koy: ajanın run_command'ında
|
|
487
|
+
`python` / `python.exe` da çalışsın (makinede Python kurulmuş olmasa bile). */
|
|
488
|
+
function pythonPathPrefix() {
|
|
489
|
+
try {
|
|
490
|
+
const d = path.dirname(embeddedPythonExe());
|
|
491
|
+
if (fs.existsSync(path.join(d, 'python.exe'))) return d + path.delimiter;
|
|
492
|
+
} catch {}
|
|
493
|
+
return '';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function envWithPathPrefix() {
|
|
497
|
+
const base = { ...process.env };
|
|
498
|
+
base.PATH = pythonPathPrefix() + (process.env.PATH || process.env.Path || '');
|
|
499
|
+
return base;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* paketlenmiş python scriptleri (src/agent/scripts) → kullanıcı scripts klasörüne tohumla */
|
|
503
|
+
function bundledScriptPath(name) {
|
|
504
|
+
return path.join(__dirname, 'scripts', name);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function seedScript(name, force = false) {
|
|
508
|
+
const src = bundledScriptPath(name);
|
|
509
|
+
try {
|
|
510
|
+
const dest = path.join(pythonScriptsDir(), name);
|
|
511
|
+
/* force: paket içindeki güncel sürüm her zaman kazanır (sistem scripti) */
|
|
512
|
+
if (fs.existsSync(src) && (force || !fs.existsSync(dest))) {
|
|
513
|
+
fs.copyFileSync(src, dest);
|
|
514
|
+
}
|
|
515
|
+
return dest;
|
|
516
|
+
} catch {
|
|
517
|
+
return path.join(pythonScriptsDir(), name);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* #20 hızlı web arama: ddgs kütüphanesi varsa onu kullan; yoksa python çoklu-motor
|
|
522
|
+
paralel; o da olmazsa JS fallback. Sonuç boş gelirse (CAPTCHA/engel) ddgs'yi
|
|
523
|
+
TEK SEFERLİK arka planda kurup tekrar dener. */
|
|
524
|
+
function pyExec(exe, args, timeoutMs) {
|
|
525
|
+
return new Promise((resolve) => {
|
|
526
|
+
try {
|
|
527
|
+
execFile(exe, args, { timeout: timeoutMs, windowsHide: true }, (err, stdout, stderr) =>
|
|
528
|
+
resolve({ err, out: String(stdout || '') + String(stderr || '') })
|
|
529
|
+
);
|
|
530
|
+
} catch (e) {
|
|
531
|
+
resolve({ err: e, out: '' });
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let _ddgsTried = null; // Promise<boolean> — oturum başına tek kurulum denemesi
|
|
537
|
+
|
|
538
|
+
function ddgsImportOk(probe) {
|
|
539
|
+
/* hızlı yerel kontrol: kütüphane import edilebiliyor mu (ağ gerektirmez) */
|
|
540
|
+
return pyExec(probe.exe, ['-c', 'import ddgs'], 15000).then((r) => !r.err);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/* pip ile TEK SEFERLİK kurulum dener; uzun sürebilir — await ETME, arka planda bırak */
|
|
544
|
+
function ensureDdgs(probe) {
|
|
545
|
+
if (_ddgsTried) return _ddgsTried;
|
|
546
|
+
_ddgsTried = (async () => {
|
|
547
|
+
try {
|
|
548
|
+
if (await ddgsImportOk(probe)) return true;
|
|
549
|
+
const pip = await pyExec(probe.exe, ['-m', 'pip', '--version'], 15000);
|
|
550
|
+
if (pip.err) return false; // gömülü python'da pip yok — sessizce vazgeç
|
|
551
|
+
const inst = await pyExec(
|
|
552
|
+
probe.exe,
|
|
553
|
+
['-m', 'pip', 'install', '--quiet', '--disable-pip-version-check', 'ddgs'],
|
|
554
|
+
120000
|
|
555
|
+
);
|
|
556
|
+
if (inst.err) return false;
|
|
557
|
+
return await ddgsImportOk(probe);
|
|
558
|
+
} catch {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
})();
|
|
562
|
+
return _ddgsTried;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function parseWsOutput(output) {
|
|
566
|
+
return String(output || '')
|
|
567
|
+
.split('\n')
|
|
568
|
+
.map((l) => l.trim())
|
|
569
|
+
.filter((l) => l.startsWith('{'))
|
|
570
|
+
.map((l) => {
|
|
571
|
+
try { return JSON.parse(l); } catch { return null; }
|
|
572
|
+
})
|
|
573
|
+
.filter(Boolean)
|
|
574
|
+
.map((x) => ({ title: String(x.title || ''), url: String(x.url || ''), snippet: String(x.snippet || ''), engine: String(x.engine || '') }));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* Obscura yaklaşımının Node-gömülü uyarlaması: Chrome TLS parmak izi taklidi.
|
|
578
|
+
curl_cffi (curl-impersonate) gömülü Python'a otomatik kurulur; ayrı program YOK.
|
|
579
|
+
Bot koruması olan sitelerden (DDG html) gerçek Chrome kimliğiyle sonuç çeker. */
|
|
580
|
+
|
|
581
|
+
let _cffiTried = null; // Promise<boolean> — oturum başına tek kurulum denemesi
|
|
582
|
+
|
|
583
|
+
function cffiImportOk(probe) {
|
|
584
|
+
return pyExec(probe.exe, ['-c', 'import curl_cffi'], 15000).then((r) => !r.err);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function ensureCffi(probe) {
|
|
588
|
+
if (_cffiTried) return _cffiTried;
|
|
589
|
+
_cffiTried = (async () => {
|
|
590
|
+
try {
|
|
591
|
+
if (await cffiImportOk(probe)) return true;
|
|
592
|
+
const pip = await pyExec(probe.exe, ['-m', 'pip', '--version'], 15000);
|
|
593
|
+
if (pip.err) return false;
|
|
594
|
+
const inst = await pyExec(
|
|
595
|
+
probe.exe,
|
|
596
|
+
['-m', 'pip', 'install', '--quiet', '--disable-pip-version-check', 'curl_cffi'],
|
|
597
|
+
300000
|
|
598
|
+
);
|
|
599
|
+
if (inst.err) return false;
|
|
600
|
+
return await cffiImportOk(probe);
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
})();
|
|
605
|
+
return _cffiTried;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/* stealth arama: curl_cffi ile DDG html — ham HTML döner, JS'te parse edilir */
|
|
609
|
+
async function stealthSearch(query, { maxResults = 8, signal } = {}) {
|
|
610
|
+
try {
|
|
611
|
+
const probe = await ensurePython({ allowDownload: true, signal });
|
|
612
|
+
if (!(await cffiImportOk(probe))) {
|
|
613
|
+
ensureCffi(probe).catch(() => {}); /* ilk çağrıda atla, kurulum arka planda */
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
const script = seedScript('stealthsearch.py', true);
|
|
617
|
+
const r = await runPy(
|
|
618
|
+
probe.exe,
|
|
619
|
+
script,
|
|
620
|
+
[encodeURIComponent(String(query || ''))],
|
|
621
|
+
process.cwd(),
|
|
622
|
+
25000,
|
|
623
|
+
signal
|
|
624
|
+
);
|
|
625
|
+
const html = String(r.output || '');
|
|
626
|
+
if (!html || html.length < 500) return null;
|
|
627
|
+
const results = parseDdgResults(html, Math.min(Math.max(Number(maxResults) || 8, 1), 12));
|
|
628
|
+
if (!results.length) return null;
|
|
629
|
+
return {
|
|
630
|
+
ok: true,
|
|
631
|
+
engine: 'stealth',
|
|
632
|
+
query,
|
|
633
|
+
results: results.map((x) => ({ ...x, engine: 'stealth' })),
|
|
634
|
+
};
|
|
635
|
+
} catch {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function webSearchFast(query, { maxResults = 8, signal } = {}) {
|
|
641
|
+
try {
|
|
642
|
+
const probe = await ensurePython({ allowDownload: true, signal });
|
|
643
|
+
const script = seedScript('websearch.py', true);
|
|
644
|
+
let r = await runPy(probe.exe, script, ['--json', '--limit', String(maxResults), query], process.cwd(), 20000, signal);
|
|
645
|
+
let results = parseWsOutput(r.output);
|
|
646
|
+
if (!results.length) {
|
|
647
|
+
/* muhtemel CAPTCHA/engel — ddgs kuruluysa hemen onunla tekrar dene;
|
|
648
|
+
kurulu değilse kurulumu arka plana bırak (bu çağrıyı bekletmez) */
|
|
649
|
+
if (await ddgsImportOk(probe)) {
|
|
650
|
+
r = await runPy(probe.exe, script, ['--json', '--limit', String(maxResults), query], process.cwd(), 20000, signal);
|
|
651
|
+
results = parseWsOutput(r.output);
|
|
652
|
+
} else {
|
|
653
|
+
ensureDdgs(probe).catch(() => {});
|
|
654
|
+
}
|
|
655
|
+
} else if (!results.some((x) => x.engine === 'ddgs')) {
|
|
656
|
+
/* arama çalışıyor ama ddgs henüz yok — arka planda sessiz kur, sonraki aramalar hızlanır */
|
|
657
|
+
ensureDdgs(probe).catch(() => {});
|
|
658
|
+
}
|
|
659
|
+
if (results.length) {
|
|
660
|
+
return { ok: true, engine: results.some((x) => x.engine === 'ddgs') ? 'ddgs' : 'python-multi', query, results };
|
|
661
|
+
}
|
|
662
|
+
} catch {}
|
|
663
|
+
/* JS fallback (tek motor DDG) */
|
|
664
|
+
return webSearch(query, { maxResults, signal });
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
let _pyProbe = null;
|
|
668
|
+
|
|
669
|
+
function probeInterpreter(exe, prefix) {
|
|
670
|
+
return new Promise((resolve) => {
|
|
671
|
+
try {
|
|
672
|
+
execFile(exe, [...(prefix || []), '--version'], { timeout: PY_PROBE_TIMEOUT, windowsHide: true }, (err, stdout, stderr) => {
|
|
673
|
+
if (err) return resolve(null);
|
|
674
|
+
const v = String(stdout || stderr || '').trim();
|
|
675
|
+
resolve(/Python\s*3/i.test(v) ? { exe, prefix: prefix || [], source: 'system', version: v } : null);
|
|
676
|
+
});
|
|
677
|
+
} catch {
|
|
678
|
+
resolve(null);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function findSystemPython() {
|
|
684
|
+
if (process.env.BEAST_PYTHON && fs.existsSync(process.env.BEAST_PYTHON)) {
|
|
685
|
+
return { exe: process.env.BEAST_PYTHON, prefix: [], source: 'env' };
|
|
686
|
+
}
|
|
687
|
+
for (const cand of [
|
|
688
|
+
['python.exe', []],
|
|
689
|
+
['python3.exe', []],
|
|
690
|
+
['py.exe', ['-3']],
|
|
691
|
+
]) {
|
|
692
|
+
const r = await probeInterpreter(cand[0], cand[1]);
|
|
693
|
+
if (r) return r;
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function httpsGetBuffer(url, redirectsLeft = 4, signal) {
|
|
699
|
+
return new Promise((resolve, reject) => {
|
|
700
|
+
const req = https.get(url, { headers: { 'User-Agent': 'BeastAgent/1.0 (+python bootstrap)' } }, (res) => {
|
|
701
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirectsLeft > 0) {
|
|
702
|
+
res.resume();
|
|
703
|
+
return resolve(httpsGetBuffer(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal));
|
|
704
|
+
}
|
|
705
|
+
if (res.statusCode !== 200) {
|
|
706
|
+
res.resume();
|
|
707
|
+
return reject(new Error(`indirme başarısız: HTTP ${res.statusCode}`));
|
|
708
|
+
}
|
|
709
|
+
const chunks = [];
|
|
710
|
+
let size = 0;
|
|
711
|
+
res.on('data', (c) => {
|
|
712
|
+
size += c.length;
|
|
713
|
+
if (size > 80 * 1024 * 1024) {
|
|
714
|
+
req.destroy(new Error('dosya çok büyük'));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
chunks.push(c);
|
|
718
|
+
});
|
|
719
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
720
|
+
res.on('error', reject);
|
|
721
|
+
});
|
|
722
|
+
req.on('error', reject);
|
|
723
|
+
if (signal) {
|
|
724
|
+
if (signal.aborted) return reject(new Error('iptal'));
|
|
725
|
+
signal.addEventListener('abort', () => req.destroy(new Error('iptal')), { once: true });
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function installEmbeddedPython(signal) {
|
|
731
|
+
const dest = path.join(beastAppDir(), 'py');
|
|
732
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
733
|
+
const zipPath = path.join(os.tmpdir(), 'beast-py-embed.zip');
|
|
734
|
+
const buf = await httpsGetBuffer(PYTHON_EMBED_URL, 4, signal);
|
|
735
|
+
fs.writeFileSync(zipPath, buf);
|
|
736
|
+
const r = await runCommand(
|
|
737
|
+
`Expand-Archive -LiteralPath "${zipPath}" -DestinationPath "${dest}" -Force`,
|
|
738
|
+
null,
|
|
739
|
+
180000
|
|
740
|
+
);
|
|
741
|
+
try { fs.unlinkSync(zipPath); } catch {}
|
|
742
|
+
if (!r.ok || !fs.existsSync(embeddedPythonExe())) {
|
|
743
|
+
throw new Error('gömülü python kurulamadı: ' + String(r.output || '').slice(0, 200));
|
|
744
|
+
}
|
|
745
|
+
/* site-packages yolunu aç — pip ile kurulabilen paketler için ön hazırlık */
|
|
746
|
+
try {
|
|
747
|
+
const pth = path.join(dest, 'python312._pth');
|
|
748
|
+
if (fs.existsSync(pth)) {
|
|
749
|
+
const cur = fs.readFileSync(pth, 'utf8');
|
|
750
|
+
if (!/import site/.test(cur)) fs.writeFileSync(pth, cur.replace(/\n*$/, '\nimport site\n'), 'utf8');
|
|
751
|
+
}
|
|
752
|
+
} catch {}
|
|
753
|
+
return { exe: embeddedPythonExe(), prefix: [], source: 'embedded', version: '3.12.8 (gömülü)' };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async function ensurePython({ allowDownload = true, signal } = {}) {
|
|
757
|
+
if (_pyProbe) {
|
|
758
|
+
try { fs.accessSync(_pyProbe.exe); return _pyProbe; } catch { _pyProbe = null; }
|
|
759
|
+
}
|
|
760
|
+
const emb = embeddedPythonExe();
|
|
761
|
+
if (fs.existsSync(emb)) {
|
|
762
|
+
_pyProbe = { exe: emb, prefix: [], source: 'embedded' };
|
|
763
|
+
return _pyProbe;
|
|
764
|
+
}
|
|
765
|
+
const sys = await findSystemPython();
|
|
766
|
+
if (sys) {
|
|
767
|
+
_pyProbe = sys;
|
|
768
|
+
return sys;
|
|
769
|
+
}
|
|
770
|
+
if (!allowDownload) throw new Error('python bulunamadı ve otomatik kurulum kapalı');
|
|
771
|
+
_pyProbe = await installEmbeddedPython(signal);
|
|
772
|
+
return _pyProbe;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function runPy(exe, scriptPath, cliArgs, cwd, timeoutMs = 120000, signal) {
|
|
776
|
+
return new Promise((resolve) => {
|
|
777
|
+
let settled = false;
|
|
778
|
+
let out = '';
|
|
779
|
+
let err = '';
|
|
780
|
+
const startedAt = Date.now();
|
|
781
|
+
const finish = (code, killed) => {
|
|
782
|
+
if (settled) return;
|
|
783
|
+
settled = true;
|
|
784
|
+
clearTimeout(timer);
|
|
785
|
+
const text =
|
|
786
|
+
(out ? out.trim() : '') +
|
|
787
|
+
(err ? (out ? '\n[stderr] ' : '') + err.trim() : '');
|
|
788
|
+
resolve({
|
|
789
|
+
ok: !killed && code === 0,
|
|
790
|
+
code,
|
|
791
|
+
ms: Date.now() - startedAt,
|
|
792
|
+
output: truncateMiddle(text || '(no output)', MAX_CMD_OUTPUT),
|
|
793
|
+
});
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
let child;
|
|
797
|
+
try {
|
|
798
|
+
child = spawn(exe, [scriptPath, ...cliArgs], {
|
|
799
|
+
cwd: cwd || process.cwd(),
|
|
800
|
+
windowsHide: true,
|
|
801
|
+
env: { ...envWithPathPrefix(), PYTHONUTF8: '1', PYTHONIOENCODING: 'utf-8' },
|
|
802
|
+
});
|
|
803
|
+
} catch (e) {
|
|
804
|
+
err += String(e.message);
|
|
805
|
+
return finish(1, false);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const timer = setTimeout(() => {
|
|
809
|
+
try { spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true }); } catch {}
|
|
810
|
+
finish(null, true);
|
|
811
|
+
err += '\n[beast] python timed out';
|
|
812
|
+
}, timeoutMs);
|
|
813
|
+
|
|
814
|
+
if (signal) {
|
|
815
|
+
if (signal.aborted) return finish(null, true);
|
|
816
|
+
signal.addEventListener('abort', () => {
|
|
817
|
+
try { spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true }); } catch {}
|
|
818
|
+
finish(null, true);
|
|
819
|
+
}, { once: true });
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
child.stdout.setEncoding('utf8');
|
|
823
|
+
child.stderr.setEncoding('utf8');
|
|
824
|
+
child.stdout.on('data', (d) => {
|
|
825
|
+
out += d;
|
|
826
|
+
if (out.length > MAX_CMD_OUTPUT * 4) out = out.slice(-MAX_CMD_OUTPUT * 2);
|
|
827
|
+
});
|
|
828
|
+
child.stderr.on('data', (d) => {
|
|
829
|
+
err += d;
|
|
830
|
+
if (err.length > MAX_CMD_OUTPUT * 4) err = err.slice(-MAX_CMD_OUTPUT * 2);
|
|
831
|
+
});
|
|
832
|
+
child.on('error', (e) => { err += String(e.message); finish(1, false); });
|
|
833
|
+
child.on('close', (code) => finish(code, false));
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/* ---------- web araçları ---------- */
|
|
838
|
+
|
|
839
|
+
const UA =
|
|
840
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36 BeastAgent/0.2';
|
|
841
|
+
|
|
842
|
+
const PRIVATE_HOST_RE =
|
|
843
|
+
/^(localhost|.*\.local|127\.|10\.|192\.168\.|169\.254\.|0\.|::1|\[::1\]|f[cd][0-9a-f]{2}:)/i;
|
|
844
|
+
|
|
845
|
+
function assertPublicHttpUrl(raw) {
|
|
846
|
+
let u;
|
|
847
|
+
try {
|
|
848
|
+
u = new URL(String(raw || ''));
|
|
849
|
+
} catch {
|
|
850
|
+
throw new Error('geçersiz URL');
|
|
851
|
+
}
|
|
852
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
853
|
+
throw new Error('yalnızca http/https desteklenir');
|
|
854
|
+
}
|
|
855
|
+
const host = u.hostname;
|
|
856
|
+
if (PRIVATE_HOST_RE.test(host)) {
|
|
857
|
+
throw new Error('yerel/ağ içi adreslere erişim engellendi');
|
|
858
|
+
}
|
|
859
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) {
|
|
860
|
+
throw new Error('yerel/ağ içi adreslere erişim engellendi');
|
|
861
|
+
}
|
|
862
|
+
if ((u.username || u.password)) {
|
|
863
|
+
throw new Error('URL içinde kullanıcı bilgisi desteklenmez');
|
|
864
|
+
}
|
|
865
|
+
return u.toString();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const ENTITIES = {
|
|
869
|
+
amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', mdash: '—', ndash: '–',
|
|
870
|
+
hellip: '…', rsquo: '\u2019', lsquo: '\u2018', ldquo: '\u201c', rdquo: '\u201d',
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
function decodeEntities(s) {
|
|
874
|
+
return String(s || '')
|
|
875
|
+
.replace(/&#x([0-9a-f]+);/gi, (_m, h) => {
|
|
876
|
+
try { return String.fromCodePoint(parseInt(h, 16)); } catch { return ''; }
|
|
877
|
+
})
|
|
878
|
+
.replace(/&#(\d+);/g, (_m, d) => {
|
|
879
|
+
try { return String.fromCodePoint(Number(d)); } catch { return ''; }
|
|
880
|
+
})
|
|
881
|
+
.replace(/&([a-z]+);/gi, (m, name) => ENTITIES[name.toLowerCase()] ?? m);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function htmlToText(html) {
|
|
885
|
+
let t = String(html || '');
|
|
886
|
+
t = t.replace(/<!--[\s\S]*?-->/g, ' ');
|
|
887
|
+
t = t.replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi, ' ');
|
|
888
|
+
t = t.replace(/<\/(p|div|li|h[1-6]|tr|section|article|br)>/gi, '\n');
|
|
889
|
+
t = t.replace(/<br\s*\/?>/gi, '\n');
|
|
890
|
+
t = t.replace(/<[^>]+>/g, ' ');
|
|
891
|
+
t = decodeEntities(t);
|
|
892
|
+
t = t.replace(/[ \t\f\v]+/g, ' ');
|
|
893
|
+
t = t.replace(/\s*\n\s*/g, '\n').replace(/\n{3,}/g, '\n\n');
|
|
894
|
+
return t.trim();
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/* opencode webfetch.ts tarzı HTML→Markdown (hafif sürüm — Turndown bağımlılığı yok):
|
|
898
|
+
başlıklar, linkler, kalık/italik, kod blokları, listeler korunur */
|
|
899
|
+
function htmlToMarkdown(html) {
|
|
900
|
+
let h = String(html || '');
|
|
901
|
+
h = h.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<!--[\s\S]*?-->/g, '');
|
|
902
|
+
h = h.replace(/<pre[^>]*>\s*<code[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_m, c) => '\n```\n' + decodeEntities(c) + '\n```\n');
|
|
903
|
+
h = h.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_m, c) => '`' + decodeEntities(c) + '`');
|
|
904
|
+
h = h.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, lvl, c) => '\n' + '#'.repeat(Number(lvl)) + ' ' + decodeEntities(c.replace(/<[^>]+>/g, '')).trim() + '\n');
|
|
905
|
+
h = h.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_m, href, c) => `[${decodeEntities(c.replace(/<[^>]+>/g, '')).trim()}](${href})`);
|
|
906
|
+
h = h.replace(/<(b|strong)[^>]*>([\s\S]*?)<\/\1>/gi, (_m, _t, c) => `**${decodeEntities(c.replace(/<[^>]+>/g, '')).trim()}**`);
|
|
907
|
+
h = h.replace(/<(i|em)[^>]*>([\s\S]*?)<\/\1>/gi, (_m, _t, c) => `*${decodeEntities(c.replace(/<[^>]+>/g, '')).trim()}*`);
|
|
908
|
+
h = h.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_m, c) => `\n- ${decodeEntities(c.replace(/<[^>]+>/g, '')).trim()}`);
|
|
909
|
+
h = h.replace(/<hr\s*\/?>/gi, '\n---\n');
|
|
910
|
+
h = h.replace(/<br\s*\/?>/gi, '\n');
|
|
911
|
+
h = h.replace(/<\/(p|div|section|article|tr|h[1-6])>/gi, '\n');
|
|
912
|
+
h = h.replace(/<[^>]+>/g, '');
|
|
913
|
+
h = decodeEntities(h);
|
|
914
|
+
h = h.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n');
|
|
915
|
+
return h.trim();
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/* DuckDuckGo HTML sonuç ayrıştırıcı — test edilebilir saf fonksiyon */
|
|
919
|
+
function parseDdgResults(html, limit = 8) {
|
|
920
|
+
const out = [];
|
|
921
|
+
const re =
|
|
922
|
+
/<a\s+([^>]*\bresult__a\b[^>]*)>([\s\S]*?)<\/a>([\s\S]*?)(?=<a\s+[^>]*\bresult__a\b|$)/g;
|
|
923
|
+
let m;
|
|
924
|
+
while ((m = re.exec(html)) && out.length < limit) {
|
|
925
|
+
const attrs = m[1] || '';
|
|
926
|
+
const hrefM = attrs.match(/href="([^"]+)"/) || attrs.match(/href='([^']+)'/);
|
|
927
|
+
let href = hrefM ? hrefM[1] : '';
|
|
928
|
+
try {
|
|
929
|
+
const u = new URL(href, 'https://duckduckgo.com');
|
|
930
|
+
const uddg = u.searchParams.get('uddg');
|
|
931
|
+
href = uddg ? decodeURIComponent(uddg) : u.toString();
|
|
932
|
+
} catch {}
|
|
933
|
+
const title = decodeEntities(m[2] || '').replace(/<[^>]+>/g, '').trim();
|
|
934
|
+
const seg = m[3] || '';
|
|
935
|
+
const snM = seg.match(/<a\s+[^>]*result__snippet[^>]*>([\s\S]*?)<\/a>/);
|
|
936
|
+
const snippet = snM ? decodeEntities(snM[1]).replace(/<[^>]+>/g, '').trim() : '';
|
|
937
|
+
if (!title || !/^https?:\/\//i.test(href)) continue;
|
|
938
|
+
if (out.some((r) => r.url === href)) continue;
|
|
939
|
+
out.push({ title, url: href, snippet });
|
|
940
|
+
}
|
|
941
|
+
return out;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function fetchWithTimeout(url, opts = {}, timeoutMs = 20000, signal) {
|
|
945
|
+
const ctrl = new AbortController();
|
|
946
|
+
const timer = setTimeout(() => ctrl.abort(new Error('zaman aşımı')), timeoutMs);
|
|
947
|
+
const onAbort = () => ctrl.abort(new Error('iptal'));
|
|
948
|
+
if (signal) {
|
|
949
|
+
if (signal.aborted) return Promise.reject(new Error('iptal'));
|
|
950
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
return await fetch(url, { ...opts, signal: ctrl.signal, redirect: 'follow' });
|
|
954
|
+
} finally {
|
|
955
|
+
clearTimeout(timer);
|
|
956
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
async function webSearch(query, { maxResults = 8, signal } = {}) {
|
|
961
|
+
const q = String(query || '').trim().slice(0, 400);
|
|
962
|
+
if (!q) return { ok: false, error: 'boş sorgu' };
|
|
963
|
+
let res;
|
|
964
|
+
try {
|
|
965
|
+
res = await fetchWithTimeout(
|
|
966
|
+
'https://html.duckduckgo.com/html/',
|
|
967
|
+
{
|
|
968
|
+
method: 'POST',
|
|
969
|
+
headers: {
|
|
970
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
971
|
+
'User-Agent': UA,
|
|
972
|
+
Accept: 'text/html',
|
|
973
|
+
},
|
|
974
|
+
body: new URLSearchParams({ q }).toString(),
|
|
975
|
+
},
|
|
976
|
+
20000,
|
|
977
|
+
signal
|
|
978
|
+
);
|
|
979
|
+
} catch (e) {
|
|
980
|
+
/* ağ hatası/rate-limit exception fırlatmasın — zincir devam edebilsin */
|
|
981
|
+
return { ok: false, error: 'DDG erişilemedi: ' + String((e && e.message) || e) };
|
|
982
|
+
}
|
|
983
|
+
if (!res.ok) return { ok: false, error: `DDG HTTP ${res.status}` };
|
|
984
|
+
const html = await res.text();
|
|
985
|
+
const results = parseDdgResults(html, Math.min(Math.max(Number(maxResults) || 8, 1), 12));
|
|
986
|
+
if (!results.length) {
|
|
987
|
+
return { ok: true, query: q, results, note: 'sonuç yok ya da DDG yanıt biçimi değişti' };
|
|
988
|
+
}
|
|
989
|
+
return { ok: true, query: q, count: results.length, results };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const MAX_FETCH_CHARS = 9000;
|
|
993
|
+
|
|
994
|
+
async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, format = 'text', timeoutMs = 30000, signal } = {}) {
|
|
995
|
+
const safe = assertPublicHttpUrl(url);
|
|
996
|
+
const t = Math.min(Math.max(Number(timeoutMs) || 30000, 1000), 120000); // opencode: default 30s, max 120s
|
|
997
|
+
const res = await fetchWithTimeout(
|
|
998
|
+
safe,
|
|
999
|
+
{ headers: { 'User-Agent': UA, Accept: 'text/html,text/plain,application/json;q=0.9,*/*;q=0.5' } },
|
|
1000
|
+
t,
|
|
1001
|
+
signal
|
|
1002
|
+
);
|
|
1003
|
+
const ctype = String(res.headers.get('content-type') || '');
|
|
1004
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
1005
|
+
const isTexty =
|
|
1006
|
+
/text\/|json|xml|javascript/i.test(ctype) || ctype === '';
|
|
1007
|
+
if (!isTexty) {
|
|
1008
|
+
return { ok: true, url: safe, status: res.status, contentType: ctype, note: 'ikili/metin olmayan içerik indirilmedi' };
|
|
1009
|
+
}
|
|
1010
|
+
const cap = 400000;
|
|
1011
|
+
let body = await res.text();
|
|
1012
|
+
if (body.length > cap) body = body.slice(0, cap);
|
|
1013
|
+
let content;
|
|
1014
|
+
if (/html/i.test(ctype)) {
|
|
1015
|
+
if (format === 'html') content = body;
|
|
1016
|
+
else if (format === 'markdown') content = htmlToMarkdown(body);
|
|
1017
|
+
else content = htmlToText(body);
|
|
1018
|
+
} else {
|
|
1019
|
+
content = body;
|
|
1020
|
+
}
|
|
1021
|
+
const truncated = content.length > maxChars;
|
|
1022
|
+
return {
|
|
1023
|
+
ok: true,
|
|
1024
|
+
url: safe,
|
|
1025
|
+
status: res.status,
|
|
1026
|
+
contentType: ctype,
|
|
1027
|
+
format,
|
|
1028
|
+
truncated,
|
|
1029
|
+
content: content.slice(0, Math.max(1000, Math.min(Number(maxChars) || MAX_FETCH_CHARS, 50000))),
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/* ================= opencode edit.ts BİREBİR PORT =================
|
|
1034
|
+
Kaynak: opencode-dev/packages/opencode/src/tool/edit.ts
|
|
1035
|
+
9 aşamalı replacer zinciri: Simple → LineTrimmed → BlockAnchor →
|
|
1036
|
+
WhitespaceNormalized → IndentationFlexible → EscapeNormalized →
|
|
1037
|
+
TrimmedBoundary → ContextAware → MultiOccurrence.
|
|
1038
|
+
Modelin old_string'i ufak girinti/boşluk farkıyla tutturamadığında zincir
|
|
1039
|
+
akıllı eşleşme bulur — "dosyayı baştan oku" döngüsü kökten kırılır. */
|
|
1040
|
+
|
|
1041
|
+
function normalizeLineEndings(text) {
|
|
1042
|
+
return text.replaceAll('\r\n', '\n');
|
|
1043
|
+
}
|
|
1044
|
+
function detectLineEnding(text) {
|
|
1045
|
+
return text.includes('\r\n') ? '\r\n' : '\n';
|
|
1046
|
+
}
|
|
1047
|
+
function convertToLineEnding(text, ending) {
|
|
1048
|
+
if (ending === '\n') return text;
|
|
1049
|
+
return text.replaceAll('\n', '\r\n');
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/* blok-anchor fallback benzerlik eşikleri (edit.ts:220-221) */
|
|
1053
|
+
const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.65;
|
|
1054
|
+
const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.65;
|
|
1055
|
+
|
|
1056
|
+
function levenshtein(a, b) {
|
|
1057
|
+
if (a === '' || b === '') return Math.max(a.length, b.length);
|
|
1058
|
+
const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
1059
|
+
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
1060
|
+
);
|
|
1061
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1062
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1063
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1064
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return matrix[a.length][b.length];
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const SimpleReplacer = function* (_content, find) {
|
|
1071
|
+
yield find;
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
const LineTrimmedReplacer = function* (content, find) {
|
|
1075
|
+
const originalLines = content.split('\n');
|
|
1076
|
+
const searchLines = find.split('\n');
|
|
1077
|
+
if (searchLines[searchLines.length - 1] === '') searchLines.pop();
|
|
1078
|
+
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
|
1079
|
+
let matches = true;
|
|
1080
|
+
for (let j = 0; j < searchLines.length; j++) {
|
|
1081
|
+
if (originalLines[i + j].trim() !== searchLines[j].trim()) {
|
|
1082
|
+
matches = false;
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (matches) {
|
|
1087
|
+
let matchStartIndex = 0;
|
|
1088
|
+
for (let k = 0; k < i; k++) matchStartIndex += originalLines[k].length + 1;
|
|
1089
|
+
let matchEndIndex = matchStartIndex;
|
|
1090
|
+
for (let k = 0; k < searchLines.length; k++) {
|
|
1091
|
+
matchEndIndex += originalLines[i + k].length;
|
|
1092
|
+
if (k < searchLines.length - 1) matchEndIndex += 1;
|
|
1093
|
+
}
|
|
1094
|
+
yield content.substring(matchStartIndex, matchEndIndex);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
const BlockAnchorReplacer = function* (content, find) {
|
|
1100
|
+
const originalLines = content.split('\n');
|
|
1101
|
+
const searchLines = find.split('\n');
|
|
1102
|
+
if (searchLines.length < 3) return;
|
|
1103
|
+
if (searchLines[searchLines.length - 1] === '') searchLines.pop();
|
|
1104
|
+
const firstLineSearch = searchLines[0].trim();
|
|
1105
|
+
const lastLineSearch = searchLines[searchLines.length - 1].trim();
|
|
1106
|
+
const searchBlockSize = searchLines.length;
|
|
1107
|
+
const maxLineDelta = Math.max(1, Math.floor(searchBlockSize * 0.25));
|
|
1108
|
+
const candidates = [];
|
|
1109
|
+
for (let i = 0; i < originalLines.length; i++) {
|
|
1110
|
+
if (originalLines[i].trim() !== firstLineSearch) continue;
|
|
1111
|
+
for (let j = i + 2; j < originalLines.length; j++) {
|
|
1112
|
+
if (originalLines[j].trim() === lastLineSearch) {
|
|
1113
|
+
const actualBlockSize = j - i + 1;
|
|
1114
|
+
if (Math.abs(actualBlockSize - searchBlockSize) <= maxLineDelta) {
|
|
1115
|
+
candidates.push({ startLine: i, endLine: j });
|
|
1116
|
+
}
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (candidates.length === 0) return;
|
|
1122
|
+
if (candidates.length === 1) {
|
|
1123
|
+
const { startLine, endLine } = candidates[0];
|
|
1124
|
+
const actualBlockSize = endLine - startLine + 1;
|
|
1125
|
+
let similarity = 0;
|
|
1126
|
+
const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2);
|
|
1127
|
+
if (linesToCheck > 0) {
|
|
1128
|
+
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
|
|
1129
|
+
const originalLine = originalLines[startLine + j].trim();
|
|
1130
|
+
const searchLine = searchLines[j].trim();
|
|
1131
|
+
const maxLen = Math.max(originalLine.length, searchLine.length);
|
|
1132
|
+
if (maxLen === 0) continue;
|
|
1133
|
+
const distance = levenshtein(originalLine, searchLine);
|
|
1134
|
+
similarity += (1 - distance / maxLen) / linesToCheck;
|
|
1135
|
+
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) break;
|
|
1136
|
+
}
|
|
1137
|
+
} else {
|
|
1138
|
+
similarity = 1.0;
|
|
1139
|
+
}
|
|
1140
|
+
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
|
|
1141
|
+
let matchStartIndex = 0;
|
|
1142
|
+
for (let k = 0; k < startLine; k++) matchStartIndex += originalLines[k].length + 1;
|
|
1143
|
+
let matchEndIndex = matchStartIndex;
|
|
1144
|
+
for (let k = startLine; k <= endLine; k++) {
|
|
1145
|
+
matchEndIndex += originalLines[k].length;
|
|
1146
|
+
if (k < endLine) matchEndIndex += 1;
|
|
1147
|
+
}
|
|
1148
|
+
yield content.substring(matchStartIndex, matchEndIndex);
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
let bestMatch = null;
|
|
1153
|
+
let maxSimilarity = -1;
|
|
1154
|
+
for (const candidate of candidates) {
|
|
1155
|
+
const { startLine, endLine } = candidate;
|
|
1156
|
+
const actualBlockSize = endLine - startLine + 1;
|
|
1157
|
+
let similarity = 0;
|
|
1158
|
+
const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2);
|
|
1159
|
+
if (linesToCheck > 0) {
|
|
1160
|
+
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
|
|
1161
|
+
const originalLine = originalLines[startLine + j].trim();
|
|
1162
|
+
const searchLine = searchLines[j].trim();
|
|
1163
|
+
const maxLen = Math.max(originalLine.length, searchLine.length);
|
|
1164
|
+
if (maxLen === 0) continue;
|
|
1165
|
+
const distance = levenshtein(originalLine, searchLine);
|
|
1166
|
+
similarity += 1 - distance / maxLen;
|
|
1167
|
+
}
|
|
1168
|
+
similarity /= linesToCheck;
|
|
1169
|
+
} else {
|
|
1170
|
+
similarity = 1.0;
|
|
1171
|
+
}
|
|
1172
|
+
if (similarity > maxSimilarity) {
|
|
1173
|
+
maxSimilarity = similarity;
|
|
1174
|
+
bestMatch = candidate;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD && bestMatch) {
|
|
1178
|
+
const { startLine, endLine } = bestMatch;
|
|
1179
|
+
let matchStartIndex = 0;
|
|
1180
|
+
for (let k = 0; k < startLine; k++) matchStartIndex += originalLines[k].length + 1;
|
|
1181
|
+
let matchEndIndex = matchStartIndex;
|
|
1182
|
+
for (let k = startLine; k <= endLine; k++) {
|
|
1183
|
+
matchEndIndex += originalLines[k].length;
|
|
1184
|
+
if (k < endLine) matchEndIndex += 1;
|
|
1185
|
+
}
|
|
1186
|
+
yield content.substring(matchStartIndex, matchEndIndex);
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
const WhitespaceNormalizedReplacer = function* (content, find) {
|
|
1191
|
+
const normalizeWhitespace = (text) => text.replace(/\s+/g, ' ').trim();
|
|
1192
|
+
const normalizedFind = normalizeWhitespace(find);
|
|
1193
|
+
const lines = content.split('\n');
|
|
1194
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1195
|
+
const line = lines[i];
|
|
1196
|
+
if (normalizeWhitespace(line) === normalizedFind) {
|
|
1197
|
+
yield line;
|
|
1198
|
+
} else {
|
|
1199
|
+
const normalizedLine = normalizeWhitespace(line);
|
|
1200
|
+
if (normalizedLine.includes(normalizedFind)) {
|
|
1201
|
+
const words = find.trim().split(/\s+/);
|
|
1202
|
+
if (words.length > 0) {
|
|
1203
|
+
const pattern = words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\s+');
|
|
1204
|
+
try {
|
|
1205
|
+
const regex = new RegExp(pattern);
|
|
1206
|
+
const match = line.match(regex);
|
|
1207
|
+
if (match) yield match[0];
|
|
1208
|
+
} catch {}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
const findLines = find.split('\n');
|
|
1214
|
+
if (findLines.length > 1) {
|
|
1215
|
+
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
1216
|
+
const block = lines.slice(i, i + findLines.length);
|
|
1217
|
+
if (normalizeWhitespace(block.join('\n')) === normalizedFind) {
|
|
1218
|
+
yield block.join('\n');
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
const IndentationFlexibleReplacer = function* (content, find) {
|
|
1225
|
+
const removeIndentation = (text) => {
|
|
1226
|
+
const lines = text.split('\n');
|
|
1227
|
+
const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
|
|
1228
|
+
if (nonEmptyLines.length === 0) return text;
|
|
1229
|
+
const minIndent = Math.min(
|
|
1230
|
+
...nonEmptyLines.map((line) => {
|
|
1231
|
+
const match = line.match(/^(\s*)/);
|
|
1232
|
+
return match ? match[1].length : 0;
|
|
1233
|
+
})
|
|
1234
|
+
);
|
|
1235
|
+
return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join('\n');
|
|
1236
|
+
};
|
|
1237
|
+
const normalizedFind = removeIndentation(find);
|
|
1238
|
+
const contentLines = content.split('\n');
|
|
1239
|
+
const findLines = find.split('\n');
|
|
1240
|
+
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
|
1241
|
+
const block = contentLines.slice(i, i + findLines.length).join('\n');
|
|
1242
|
+
if (removeIndentation(block) === normalizedFind) yield block;
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
const EscapeNormalizedReplacer = function* (content, find) {
|
|
1247
|
+
const unescapeString = (str) =>
|
|
1248
|
+
str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
|
|
1249
|
+
switch (capturedChar) {
|
|
1250
|
+
case 'n': return '\n';
|
|
1251
|
+
case 't': return '\t';
|
|
1252
|
+
case 'r': return '\r';
|
|
1253
|
+
case "'": return "'";
|
|
1254
|
+
case '"': return '"';
|
|
1255
|
+
case '`': return '`';
|
|
1256
|
+
case '\\': return '\\';
|
|
1257
|
+
case '\n': return '\n';
|
|
1258
|
+
case '$': return '$';
|
|
1259
|
+
default: return match;
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
const unescapedFind = unescapeString(find);
|
|
1263
|
+
if (content.includes(unescapedFind)) yield unescapedFind;
|
|
1264
|
+
const lines = content.split('\n');
|
|
1265
|
+
const findLines = unescapedFind.split('\n');
|
|
1266
|
+
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
1267
|
+
const block = lines.slice(i, i + findLines.length).join('\n');
|
|
1268
|
+
if (unescapeString(block) === unescapedFind) yield block;
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
const MultiOccurrenceReplacer = function* (content, find) {
|
|
1273
|
+
let startIndex = 0;
|
|
1274
|
+
while (true) {
|
|
1275
|
+
const index = content.indexOf(find, startIndex);
|
|
1276
|
+
if (index === -1) break;
|
|
1277
|
+
yield find;
|
|
1278
|
+
startIndex = index + find.length;
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
const TrimmedBoundaryReplacer = function* (content, find) {
|
|
1283
|
+
const trimmedFind = find.trim();
|
|
1284
|
+
if (trimmedFind === find) return;
|
|
1285
|
+
if (content.includes(trimmedFind)) yield trimmedFind;
|
|
1286
|
+
const lines = content.split('\n');
|
|
1287
|
+
const findLines = find.split('\n');
|
|
1288
|
+
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
1289
|
+
const block = lines.slice(i, i + findLines.length).join('\n');
|
|
1290
|
+
if (block.trim() === trimmedFind) yield block;
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
const ContextAwareReplacer = function* (content, find) {
|
|
1295
|
+
const findLines = find.split('\n');
|
|
1296
|
+
if (findLines.length < 3) return;
|
|
1297
|
+
if (findLines[findLines.length - 1] === '') findLines.pop();
|
|
1298
|
+
const contentLines = content.split('\n');
|
|
1299
|
+
const firstLine = findLines[0].trim();
|
|
1300
|
+
const lastLine = findLines[findLines.length - 1].trim();
|
|
1301
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
1302
|
+
if (contentLines[i].trim() !== firstLine) continue;
|
|
1303
|
+
for (let j = i + 2; j < contentLines.length; j++) {
|
|
1304
|
+
if (contentLines[j].trim() === lastLine) {
|
|
1305
|
+
const blockLines = contentLines.slice(i, j + 1);
|
|
1306
|
+
const block = blockLines.join('\n');
|
|
1307
|
+
if (blockLines.length === findLines.length) {
|
|
1308
|
+
let matchingLines = 0;
|
|
1309
|
+
let totalNonEmptyLines = 0;
|
|
1310
|
+
for (let k = 1; k < blockLines.length - 1; k++) {
|
|
1311
|
+
const blockLine = blockLines[k].trim();
|
|
1312
|
+
const findLine = findLines[k].trim();
|
|
1313
|
+
if (blockLine.length > 0 || findLine.length > 0) {
|
|
1314
|
+
totalNonEmptyLines++;
|
|
1315
|
+
if (blockLine === findLine) matchingLines++;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
|
|
1319
|
+
yield block;
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
function isDisproportionateMatch(search, oldString) {
|
|
1330
|
+
const oldLines = oldString.split('\n').length;
|
|
1331
|
+
const searchLines = search.split('\n').length;
|
|
1332
|
+
if (searchLines >= Math.max(oldLines + 3, oldLines * 2)) return true;
|
|
1333
|
+
if (oldLines === 1) return false;
|
|
1334
|
+
return search.trim().length > Math.max(oldString.trim().length + 500, oldString.trim().length * 4);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/* edit.ts:682-729 replace() — zinciri sırayla dener; tek eşleşme şart,
|
|
1338
|
+
replaceAll'de tümünü değiştirir; hata mesajları BİREBİR opencode */
|
|
1339
|
+
function ocReplace(content, oldString, newString, replaceAll = false) {
|
|
1340
|
+
if (oldString === newString) {
|
|
1341
|
+
throw new Error('No changes to apply: oldString and newString are identical.');
|
|
1342
|
+
}
|
|
1343
|
+
if (oldString === '') {
|
|
1344
|
+
throw new Error(
|
|
1345
|
+
'oldString cannot be empty when editing an existing file. Provide the exact text to replace, or use write_file for an intentional full-file replacement.'
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
let notFound = true;
|
|
1349
|
+
for (const replacer of [
|
|
1350
|
+
SimpleReplacer,
|
|
1351
|
+
LineTrimmedReplacer,
|
|
1352
|
+
BlockAnchorReplacer,
|
|
1353
|
+
WhitespaceNormalizedReplacer,
|
|
1354
|
+
IndentationFlexibleReplacer,
|
|
1355
|
+
EscapeNormalizedReplacer,
|
|
1356
|
+
TrimmedBoundaryReplacer,
|
|
1357
|
+
ContextAwareReplacer,
|
|
1358
|
+
MultiOccurrenceReplacer,
|
|
1359
|
+
]) {
|
|
1360
|
+
for (const search of replacer(content, oldString)) {
|
|
1361
|
+
const index = content.indexOf(search);
|
|
1362
|
+
if (index === -1) continue;
|
|
1363
|
+
notFound = false;
|
|
1364
|
+
if (isDisproportionateMatch(search, oldString)) {
|
|
1365
|
+
throw new Error(
|
|
1366
|
+
'Refusing replacement because the matched span is much larger than oldString. Re-read the file and provide the full exact oldString for the intended replacement.'
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
if (replaceAll) {
|
|
1370
|
+
return { result: content.replaceAll(search, newString), replacements: content.split(search).length - 1 };
|
|
1371
|
+
}
|
|
1372
|
+
const lastIndex = content.lastIndexOf(search);
|
|
1373
|
+
if (index !== lastIndex) continue;
|
|
1374
|
+
return { result: content.substring(0, index) + newString + content.substring(index + search.length), replacements: 1 };
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (notFound) {
|
|
1378
|
+
throw new Error(
|
|
1379
|
+
'Could not find oldString in the file. It must match exactly, including whitespace, indentation, and line endings.'
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
throw new Error('Found multiple matches for oldString. Provide more surrounding context to make the match unique.');
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/* ---------- opencode diff portu (npm "diff" paketinin diffLines karşılığı) ----------
|
|
1386
|
+
LCS tabanlı satır diff'i: additions/deletions sayımı (edit.ts:175-180) ve
|
|
1387
|
+
UI split-diff görünümü için kırpılmış bölge (trimDiff mantığı). */
|
|
1388
|
+
function diffLineCounts(aText, bText) {
|
|
1389
|
+
/* bos metin = 0 satir (opencode npm diffLines davranisi) */
|
|
1390
|
+
const a = aText ? String(aText).split('\n') : [];
|
|
1391
|
+
const b = bText ? String(bText).split('\n') : [];
|
|
1392
|
+
let p = 0;
|
|
1393
|
+
while (p < a.length && p < b.length && a[p] === b[p]) p++;
|
|
1394
|
+
let ea = a.length - 1;
|
|
1395
|
+
let eb = b.length - 1;
|
|
1396
|
+
while (ea >= p && eb >= p && a[ea] === b[eb]) { ea--; eb--; }
|
|
1397
|
+
const midA = a.slice(p, ea + 1);
|
|
1398
|
+
const midB = b.slice(p, eb + 1);
|
|
1399
|
+
const n = midA.length;
|
|
1400
|
+
const m = midB.length;
|
|
1401
|
+
if (!n && !m) return { additions: 0, deletions: 0 };
|
|
1402
|
+
if (n * m > 400000 || n > 1500 || m > 1500) return { additions: m, deletions: n };
|
|
1403
|
+
const w = m + 1;
|
|
1404
|
+
const dp = new Int32Array((n + 1) * w);
|
|
1405
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
1406
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
1407
|
+
dp[i * w + j] =
|
|
1408
|
+
midA[i] === midB[j]
|
|
1409
|
+
? dp[(i + 1) * w + j + 1] + 1
|
|
1410
|
+
: Math.max(dp[(i + 1) * w + j], dp[i * w + j + 1]);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
const matched = dp[0];
|
|
1414
|
+
return { additions: m - matched, deletions: n - matched };
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
const DIFF_REGION_CTX = 3; /* değişiklik etrafında kaç bağlam satırı gösterilir */
|
|
1418
|
+
const DIFF_REGION_CAP = 3500; /* UI'ye giden bölge başına karakter tavanı */
|
|
1419
|
+
|
|
1420
|
+
function capDiffText(s) {
|
|
1421
|
+
const t = String(s || '');
|
|
1422
|
+
return t.length > DIFF_REGION_CAP ? t.slice(0, DIFF_REGION_CAP) + '\n…' : t;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/* Değişen bölgeyi ±DIFF_REGION_CTX bağlam satırıyla kırpar (opencode trimDiff
|
|
1426
|
+
mantığı — tüm dosya yerine yalnız değişen kısım UI'ye gider). */
|
|
1427
|
+
function diffRegion(before, after) {
|
|
1428
|
+
const a = String(before || '').split('\n');
|
|
1429
|
+
const b = String(after || '').split('\n');
|
|
1430
|
+
let p = 0;
|
|
1431
|
+
while (p < a.length && p < b.length && a[p] === b[p]) p++;
|
|
1432
|
+
let ea = a.length - 1;
|
|
1433
|
+
let eb = b.length - 1;
|
|
1434
|
+
while (ea >= p && eb >= p && a[ea] === b[eb]) { ea--; eb--; }
|
|
1435
|
+
const start = Math.max(0, p - DIFF_REGION_CTX);
|
|
1436
|
+
const beforeRegion = a.slice(start, Math.min(a.length, ea + 1 + DIFF_REGION_CTX)).join('\n');
|
|
1437
|
+
const afterRegion = b.slice(start, Math.min(b.length, eb + 1 + DIFF_REGION_CTX)).join('\n');
|
|
1438
|
+
return { before: capDiffText(beforeRegion), after: capDiffText(afterRegion), startLine: start + 1 };
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/* ---------- read_file disk-cache (opencode file-state portu) ----------
|
|
1442
|
+
mtime+size değişmemişse dosya yeniden diskten OKUNMAZ — cache'ten döner.
|
|
1443
|
+
edit_file/write_file kendi yazdıklarından sonra cache'i düşürür; böylece
|
|
1444
|
+
sonraki okuma daima güncel içerik verir. */
|
|
1445
|
+
const _readCache = new Map(); /* abs → { mtimeMs, size, raw } */
|
|
1446
|
+
|
|
1447
|
+
function readCacheGet(abs, st) {
|
|
1448
|
+
const hit = _readCache.get(abs);
|
|
1449
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) return hit.raw;
|
|
1450
|
+
const raw = fs.readFileSync(abs, 'utf8');
|
|
1451
|
+
if (_readCache.size > 200) _readCache.delete(_readCache.keys().next().value);
|
|
1452
|
+
_readCache.set(abs, { mtimeMs: st.mtimeMs, size: st.size, raw });
|
|
1453
|
+
return raw;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function readCacheDrop(abs) {
|
|
1457
|
+
_readCache.delete(abs);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const definitions = [
|
|
1461
|
+
{
|
|
1462
|
+
type: 'function',
|
|
1463
|
+
function: {
|
|
1464
|
+
name: 'run_command',
|
|
1465
|
+
description:
|
|
1466
|
+
'Executes a given PowerShell command on the user\'s Windows machine in the workspace directory and returns combined stdout/stderr. Use this tool for terminal operations like builds, git, npm installs, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead: read_file, write_file, edit_file, grep, glob.',
|
|
1467
|
+
parameters: {
|
|
1468
|
+
type: 'object',
|
|
1469
|
+
properties: {
|
|
1470
|
+
command: { type: 'string', description: 'PowerShell command line to execute' },
|
|
1471
|
+
timeout_ms: { type: 'number', description: 'Optional timeout in ms (default 120000)' },
|
|
1472
|
+
},
|
|
1473
|
+
required: ['command'],
|
|
1474
|
+
},
|
|
1475
|
+
},
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
type: 'function',
|
|
1479
|
+
function: {
|
|
1480
|
+
name: 'read_file',
|
|
1481
|
+
/* opencode read.txt BİREBİR port (parametre adı path olarak kaldı) */
|
|
1482
|
+
description:
|
|
1483
|
+
'Read a file or directory from the local filesystem. If the path does not exist, an error is returned.\n\n' +
|
|
1484
|
+
'Usage:\n' +
|
|
1485
|
+
'- By default, this tool returns up to 2000 lines from the start of the file.\n' +
|
|
1486
|
+
'- The offset parameter is the line number to start reading from (1-indexed).\n' +
|
|
1487
|
+
'- To read later sections, call this tool again with a larger offset — NEVER re-read from the start of the file.\n' +
|
|
1488
|
+
'- Use the grep tool to find specific content in large files or files with long lines.\n' +
|
|
1489
|
+
'- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\n' +
|
|
1490
|
+
'- Contents are returned with each line prefixed by its line number as `<line>: <content>`. For example, if a file has contents "foo\\n", you will receive "1: foo\\n".\n' +
|
|
1491
|
+
'- Any line longer than 2000 characters is truncated.\n' +
|
|
1492
|
+
'- Call this tool in parallel when you know there are multiple files you want to read.\n' +
|
|
1493
|
+
'- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\n' +
|
|
1494
|
+
'- File contents you read STAY in your context for the WHOLE session. Do NOT re-read a file you already read: chain edits from your previous read plus your own edit_file/write_file results. Re-reading the same file (same or overlapping range) wastes turns and is forbidden unless the file actually changed on disk outside your own edits.',
|
|
1495
|
+
parameters: {
|
|
1496
|
+
type: 'object',
|
|
1497
|
+
properties: {
|
|
1498
|
+
path: { type: 'string', description: 'The path to the file to read' },
|
|
1499
|
+
offset: { type: 'number', description: 'The line number to start reading from (1-indexed)' },
|
|
1500
|
+
limit: { type: 'number', description: 'The maximum number of lines to read (defaults to 2000)' },
|
|
1501
|
+
},
|
|
1502
|
+
required: ['path'],
|
|
1503
|
+
},
|
|
1504
|
+
},
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
type: 'function',
|
|
1508
|
+
function: {
|
|
1509
|
+
name: 'write_file',
|
|
1510
|
+
/* opencode write.txt BİREBİR port */
|
|
1511
|
+
description:
|
|
1512
|
+
'Writes a file to the local filesystem.\n\n' +
|
|
1513
|
+
'Usage:\n' +
|
|
1514
|
+
'- This tool will overwrite the existing file if there is one at the provided path.\n' +
|
|
1515
|
+
'- If this is an existing file, you MUST use the read_file tool first to read the file\'s contents. This tool will fail if you did not read the file first.\n' +
|
|
1516
|
+
'- ALWAYS prefer editing existing files in the codebase with edit_file. NEVER write new files unless explicitly required.\n' +
|
|
1517
|
+
'- The result includes additions/deletions counts — the change is APPLIED to disk immediately; do NOT read the file again to verify.',
|
|
1518
|
+
parameters: {
|
|
1519
|
+
type: 'object',
|
|
1520
|
+
properties: {
|
|
1521
|
+
path: { type: 'string', description: 'The path to the file to write' },
|
|
1522
|
+
content: { type: 'string', description: 'The content to write to the file' },
|
|
1523
|
+
},
|
|
1524
|
+
required: ['path', 'content'],
|
|
1525
|
+
},
|
|
1526
|
+
},
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
type: 'function',
|
|
1530
|
+
function: {
|
|
1531
|
+
name: 'edit_file',
|
|
1532
|
+
/* opencode edit.txt BİREBİR port (parametre adları snake_case kaldı) */
|
|
1533
|
+
description:
|
|
1534
|
+
'Performs exact string replacements in files.\n\n' +
|
|
1535
|
+
'Usage:\n' +
|
|
1536
|
+
'- You must use your read_file tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.\n' +
|
|
1537
|
+
'- When editing text from read_file output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + colon + space (e.g., `1: `). Everything after that space is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.\n' +
|
|
1538
|
+
'- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n' +
|
|
1539
|
+
'- The edit will FAIL if `old_string` is not found in the file with an error "oldString not found in content".\n' +
|
|
1540
|
+
'- The edit will FAIL if `old_string` is found multiple times in the file with an error "Found multiple matches for oldString. Provide more surrounding lines in old_string to identify the correct match." Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.\n' +
|
|
1541
|
+
'- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.\n' +
|
|
1542
|
+
'- The result includes additions/deletions counts — the change is APPLIED to disk immediately; do NOT read the file again to verify.\n' +
|
|
1543
|
+
'- Chain follow-up edits from what you already read plus your own previous edits — do NOT re-read the file between edits. Multiple edit_file calls to different files can be issued in PARALLEL in one turn.',
|
|
1544
|
+
parameters: {
|
|
1545
|
+
type: 'object',
|
|
1546
|
+
properties: {
|
|
1547
|
+
path: { type: 'string', description: 'The path to the file to modify' },
|
|
1548
|
+
old_string: { type: 'string', description: 'The text to replace' },
|
|
1549
|
+
new_string: { type: 'string', description: 'The text to replace it with (must be different from old_string)' },
|
|
1550
|
+
replace_all: { type: 'boolean', description: 'Replace all occurrences of old_string (default false)' },
|
|
1551
|
+
},
|
|
1552
|
+
required: ['path', 'old_string', 'new_string'],
|
|
1553
|
+
},
|
|
1554
|
+
},
|
|
1555
|
+
},
|
|
1556
|
+
{
|
|
1557
|
+
type: 'function',
|
|
1558
|
+
function: {
|
|
1559
|
+
name: 'grep',
|
|
1560
|
+
description:
|
|
1561
|
+
'Fast content search tool that works with any codebase size. Searches file contents using regular expressions (case-SENSITIVE by default — pass case_insensitive:true to ignore case); supports full regex syntax (eg. "log.*Error", "function\\s+\\w+"). Filter files by pattern with include (eg. "*.js", "*.{ts,tsx}"). Returns grouped matches as `<path>:` + ` Line N: text`, capped at 100 matches. node_modules/.git and similar dirs are skipped. Use this to find where functions/symbols/errors live before editing.',
|
|
1562
|
+
parameters: {
|
|
1563
|
+
type: 'object',
|
|
1564
|
+
properties: {
|
|
1565
|
+
pattern: { type: 'string', description: 'Regular expression' },
|
|
1566
|
+
path: { type: 'string', description: 'File or directory to search; defaults to workspace root' },
|
|
1567
|
+
include: { type: 'string', description: 'Glob filter like "*.js" or "*.{ts,tsx}"' },
|
|
1568
|
+
case_insensitive: { type: 'boolean', description: 'Ignore case (default false — search is case-sensitive)' },
|
|
1569
|
+
},
|
|
1570
|
+
required: ['pattern'],
|
|
1571
|
+
},
|
|
1572
|
+
},
|
|
1573
|
+
},
|
|
1574
|
+
{
|
|
1575
|
+
type: 'function',
|
|
1576
|
+
function: {
|
|
1577
|
+
name: 'glob',
|
|
1578
|
+
description:
|
|
1579
|
+
'Fast file pattern matching tool that works with any codebase size. Supports glob patterns like "**/*.js" or "src/**/*.ts". Returns matching file paths (max 100; more specific pattern/path if truncated). Use when you need to find files by name patterns; batch multiple speculative searches in one turn.',
|
|
1580
|
+
parameters: {
|
|
1581
|
+
type: 'object',
|
|
1582
|
+
properties: {
|
|
1583
|
+
pattern: { type: 'string', description: 'Glob pattern, e.g. "src/**/*.ts"' },
|
|
1584
|
+
path: { type: 'string', description: 'Directory to search; defaults to workspace root' },
|
|
1585
|
+
},
|
|
1586
|
+
required: ['pattern'],
|
|
1587
|
+
},
|
|
1588
|
+
},
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
type: 'function',
|
|
1592
|
+
function: {
|
|
1593
|
+
name: 'list_dir',
|
|
1594
|
+
description: 'List entries of a directory (localeCompare sorted, directories suffixed with `/`, max 500 entries) with sizes and types.',
|
|
1595
|
+
parameters: {
|
|
1596
|
+
type: 'object',
|
|
1597
|
+
properties: {
|
|
1598
|
+
path: { type: 'string', description: 'Defaults to workspace root' },
|
|
1599
|
+
},
|
|
1600
|
+
required: [],
|
|
1601
|
+
},
|
|
1602
|
+
},
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
type: 'function',
|
|
1606
|
+
function: {
|
|
1607
|
+
name: 'webfetch',
|
|
1608
|
+
description:
|
|
1609
|
+
'- Fetches content from a specified URL\n' +
|
|
1610
|
+
'- Takes a URL and optional format as input\n' +
|
|
1611
|
+
'- Fetches the URL content, converts to requested format (markdown by default)\n' +
|
|
1612
|
+
'- Returns the content in the specified format\n' +
|
|
1613
|
+
'- Use this tool when you need to retrieve and analyze web content\n' +
|
|
1614
|
+
'- IMPORTANT: if another tool is present that offers better web fetching capabilities, is more targeted to the task, or has fewer restrictions, prefer using that tool instead of this one.\n' +
|
|
1615
|
+
'- The URL must be a fully-formed valid URL\n' +
|
|
1616
|
+
'- HTTP URLs will be automatically upgraded to HTTPS\n' +
|
|
1617
|
+
'- Format options: "text" (default), "markdown", or "html"\n' +
|
|
1618
|
+
'- Local/private network addresses are blocked; timeout default 30s (max 120s)',
|
|
1619
|
+
parameters: {
|
|
1620
|
+
type: 'object',
|
|
1621
|
+
properties: {
|
|
1622
|
+
url: { type: 'string', format: 'uri', description: 'The URL to fetch content from' },
|
|
1623
|
+
format: { type: 'string', enum: ['text', 'markdown', 'html'], description: 'The format to return the content in (defaults to text)' },
|
|
1624
|
+
timeout: { type: 'number', description: 'Optional timeout in seconds (max 120)' },
|
|
1625
|
+
max_chars: { type: 'number', description: 'Output character cap (default 9000, max 50000)' },
|
|
1626
|
+
},
|
|
1627
|
+
required: ['url'],
|
|
1628
|
+
},
|
|
1629
|
+
},
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
type: 'function',
|
|
1633
|
+
function: {
|
|
1634
|
+
name: 'web_search',
|
|
1635
|
+
description:
|
|
1636
|
+
'FAST web search with an automatic chain (order is configurable in Ayarlar → Web Arama): local SearXNG (free, unlimited, auto-prioritized when running — start with `beast searxng`), built-in browser (real Chromium searching GOOGLE directly with AI Mode — no bot protection; the response may include an `ai` field holding Google\'s own AI answer), TinyFish API (only if a key is configured), then Python multi-engine (ddgs / DuckDuckGo+Bing+Mojeek). If the browser hits CAPTCHA/unusual traffic it is skipped for 10 minutes and the next engine takes over. Returns {ai?, results[{title,url,snippet}]}. Use when fresh or external info is needed; skip for things you already know.',
|
|
1637
|
+
parameters: {
|
|
1638
|
+
type: 'object',
|
|
1639
|
+
properties: {
|
|
1640
|
+
query: { type: 'string' },
|
|
1641
|
+
max_results: { type: 'number', description: '1-12, default 8' },
|
|
1642
|
+
},
|
|
1643
|
+
required: ['query'],
|
|
1644
|
+
},
|
|
1645
|
+
},
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
type: 'function',
|
|
1649
|
+
function: {
|
|
1650
|
+
name: 'http_fetch',
|
|
1651
|
+
description:
|
|
1652
|
+
'Fetch one URL and return its text content (HTML converted to plain text, JSON as-is). Local/private network addresses are blocked.',
|
|
1653
|
+
parameters: {
|
|
1654
|
+
type: 'object',
|
|
1655
|
+
properties: {
|
|
1656
|
+
url: { type: 'string', format: 'uri' },
|
|
1657
|
+
max_chars: { type: 'number', description: 'default 9000' },
|
|
1658
|
+
},
|
|
1659
|
+
required: ['url'],
|
|
1660
|
+
},
|
|
1661
|
+
},
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
type: 'function',
|
|
1665
|
+
function: {
|
|
1666
|
+
name: 'deep_search',
|
|
1667
|
+
description:
|
|
1668
|
+
'AGENTIC DEEP RESEARCH — use when web_search is not enough or the answer was NOT found on the first try. Runs 1-4 query variants IN PARALLEL (rephrase, synonyms, Turkish + English spellings of the same question), merges + dedupes results, then AUTOMATICALLY opens and reads the top pages with a HIDDEN real-Chromium browser (JS/SPA pages work; the visible panel is NOT touched) and returns full-text excerpts. Ideal for: multi-angle questions (price comparison, reviews, specs, "find everything about X"), Turkish queries that miss results, pages that need JS rendering. Returns {queries, results[{title,url,snippet}], pages[{url,title,content}]}.',
|
|
1669
|
+
parameters: {
|
|
1670
|
+
type: 'object',
|
|
1671
|
+
properties: {
|
|
1672
|
+
queries: {
|
|
1673
|
+
type: 'array',
|
|
1674
|
+
maxItems: 4,
|
|
1675
|
+
items: { type: 'string' },
|
|
1676
|
+
description: '1-4 query variants; e.g. ["iphone 16 fiyat", "iphone 16 price turkey", "iphone 16 technosa"]',
|
|
1677
|
+
},
|
|
1678
|
+
max_results: { type: 'number', description: 'merged result cap, default 16' },
|
|
1679
|
+
read_top: { type: 'number', description: 'how many top results to auto-read fully (0-6, default 3); 0 = results only' },
|
|
1680
|
+
},
|
|
1681
|
+
required: ['queries'],
|
|
1682
|
+
},
|
|
1683
|
+
},
|
|
1684
|
+
},
|
|
1685
|
+
{
|
|
1686
|
+
type: 'function',
|
|
1687
|
+
function: {
|
|
1688
|
+
name: 'python_run',
|
|
1689
|
+
description:
|
|
1690
|
+
"Run Python (system interpreter, or Beast auto-installs a portable embedded runtime on first use). Either pass inline `code`, or `script` = filename inside the Beast scripts library (%APPDATA%\\beast\\scripts — e.g. news.py haber toplayıcı) or an absolute path. Use for scraping, RSS/haber toplama, veri analizi, regex/parsing işleri. stdout+stderr returned; scripts should print results.",
|
|
1691
|
+
parameters: {
|
|
1692
|
+
type: 'object',
|
|
1693
|
+
properties: {
|
|
1694
|
+
code: { type: 'string', description: 'Inline Python source (use this OR script)' },
|
|
1695
|
+
script: { type: 'string', description: 'Script name in beast scripts dir or absolute path' },
|
|
1696
|
+
args: {
|
|
1697
|
+
type: 'array',
|
|
1698
|
+
items: { type: 'string' },
|
|
1699
|
+
description: 'CLI arguments passed to the script, e.g. ["--limit","5","--json"]',
|
|
1700
|
+
},
|
|
1701
|
+
timeout_ms: { type: 'number', description: 'default 120000' },
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
},
|
|
1705
|
+
},
|
|
1706
|
+
];
|
|
1707
|
+
|
|
1708
|
+
async function exec(name, args, ctx) {
|
|
1709
|
+
const cwd = ctx.cwd;
|
|
1710
|
+
try {
|
|
1711
|
+
switch (name) {
|
|
1712
|
+
case 'run_command': {
|
|
1713
|
+
/* opencode shell.ts kuralları: default 120s, negatif timeout reddi */
|
|
1714
|
+
const tRaw = Number(args.timeout_ms);
|
|
1715
|
+
if (Number.isFinite(tRaw) && args.timeout_ms != null && tRaw <= 0) {
|
|
1716
|
+
return JSON.stringify({ ok: false, error: `Invalid timeout value: ${tRaw}. Timeout must be a positive number.` });
|
|
1717
|
+
}
|
|
1718
|
+
const t = Number.isFinite(tRaw) && tRaw > 0 ? tRaw : 120000;
|
|
1719
|
+
const shell = String(args.shell || 'powershell').toLowerCase();
|
|
1720
|
+
const r =
|
|
1721
|
+
shell === 'bash' || shell === 'sh'
|
|
1722
|
+
? await runBashCommand(String(args.command || ''), cwd, t, ctx.signal)
|
|
1723
|
+
: await runShellCommand(String(args.command || ''), cwd, t, ctx.signal);
|
|
1724
|
+
const b = boundToolOutput((r && r.output) || '');
|
|
1725
|
+
return JSON.stringify({
|
|
1726
|
+
ok: !!(r && r.ok),
|
|
1727
|
+
code: r && r.code,
|
|
1728
|
+
output: b.text || '(no output)',
|
|
1729
|
+
...(b.outputFile ? { outputFile: b.outputFile } : {}),
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
case 'edit_file': {
|
|
1733
|
+
/* opencode edit.ts execute BİREBİR port — replacer zinciri + satır sonu
|
|
1734
|
+
normalizasyonu + diff metadata (additions/deletions + UI diffView) */
|
|
1735
|
+
const filePath = safeResolve(String(args.path || args.filePath || ''), cwd);
|
|
1736
|
+
const oldS = String(args.old_string ?? args.oldString ?? '');
|
|
1737
|
+
const newS = String(args.new_string ?? args.newString ?? '');
|
|
1738
|
+
try {
|
|
1739
|
+
if (oldS === newS) {
|
|
1740
|
+
throw new Error('No changes to apply: oldString and newString are identical.');
|
|
1741
|
+
}
|
|
1742
|
+
/* boş oldString = YENİ dosya oluşturma yolu (edit.ts:90-121) */
|
|
1743
|
+
if (oldS === '') {
|
|
1744
|
+
if (fs.existsSync(filePath)) {
|
|
1745
|
+
throw new Error(
|
|
1746
|
+
'oldString cannot be empty when editing an existing file. Provide the exact text to replace, or use write_file for an intentional full-file replacement.'
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1750
|
+
fs.writeFileSync(filePath, newS, 'utf8');
|
|
1751
|
+
readCacheDrop(filePath);
|
|
1752
|
+
const counts = diffLineCounts('', newS);
|
|
1753
|
+
const out = { ok: true, path: filePath, note: 'Edit applied successfully.', replacements: 1, ...counts };
|
|
1754
|
+
if (ctx.wantDiff) out.diffView = { path: filePath, before: '', after: capDiffText(newS), startLine: 1, ...counts };
|
|
1755
|
+
return JSON.stringify(out);
|
|
1756
|
+
}
|
|
1757
|
+
if (!fs.existsSync(filePath)) throw new Error(`File ${filePath} not found`);
|
|
1758
|
+
if (fs.statSync(filePath).isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`);
|
|
1759
|
+
const contentOld = fs.readFileSync(filePath, 'utf8');
|
|
1760
|
+
/* satır sonu stili dosyadan alınır, old/new buna çevrilir (edit.ts:129-131) */
|
|
1761
|
+
const ending = detectLineEnding(contentOld);
|
|
1762
|
+
const oldN = convertToLineEnding(normalizeLineEndings(oldS), ending);
|
|
1763
|
+
const newN = convertToLineEnding(normalizeLineEndings(newS), ending);
|
|
1764
|
+
const rep = ocReplace(contentOld, oldN, newN, !!(args.replace_all || args.replaceAll));
|
|
1765
|
+
const contentNew = rep.result;
|
|
1766
|
+
fs.writeFileSync(filePath, contentNew, 'utf8');
|
|
1767
|
+
readCacheDrop(filePath); /* sonraki read_file taze içerik okusun */
|
|
1768
|
+
const counts = diffLineCounts(contentOld, contentNew);
|
|
1769
|
+
const out = {
|
|
1770
|
+
ok: true,
|
|
1771
|
+
path: filePath,
|
|
1772
|
+
note: 'Edit applied successfully.',
|
|
1773
|
+
replacements: rep.replacements,
|
|
1774
|
+
...counts,
|
|
1775
|
+
};
|
|
1776
|
+
if (ctx.wantDiff) {
|
|
1777
|
+
out.diffView = { path: filePath, ...diffRegion(contentOld, contentNew), ...counts };
|
|
1778
|
+
}
|
|
1779
|
+
return JSON.stringify(out);
|
|
1780
|
+
} catch (e) {
|
|
1781
|
+
return JSON.stringify({ ok: false, error: String((e && e.message) || e) });
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
case 'read_file': {
|
|
1785
|
+
/* opencode read.ts BİREBİR port: fuzzy not-found, binary tespiti,
|
|
1786
|
+
dizin okuma, `N: içerik` formatı, 2000 satır / 2000 karakter /
|
|
1787
|
+
50KB tavanları + opencode devam footer'ları */
|
|
1788
|
+
const abs = safeResolve(String(args.path || ''), cwd);
|
|
1789
|
+
if (!fs.existsSync(abs)) {
|
|
1790
|
+
const sibs = fuzzySiblings(abs);
|
|
1791
|
+
const hint = sibs.length ? '\nDid you mean one of these?\n' + sibs.map((s) => `- ${path.join(path.dirname(abs), s)}`).join('\n') : '';
|
|
1792
|
+
return JSON.stringify({ ok: false, error: `File not found: ${abs}${hint}` });
|
|
1793
|
+
}
|
|
1794
|
+
const st = fs.statSync(abs);
|
|
1795
|
+
/* DİZİN OKUMA (opencode read.ts dizin modu): localeCompare sıralı,
|
|
1796
|
+
klasörler `/` ile, offset/limit sayfalı */
|
|
1797
|
+
if (st.isDirectory()) {
|
|
1798
|
+
let names = fs
|
|
1799
|
+
.readdirSync(abs)
|
|
1800
|
+
.sort((a, b) => a.localeCompare(b))
|
|
1801
|
+
.map((name) => {
|
|
1802
|
+
let isDir = false;
|
|
1803
|
+
try { isDir = fs.statSync(path.join(abs, name)).isDirectory(); } catch {}
|
|
1804
|
+
return isDir ? name + '/' : name;
|
|
1805
|
+
});
|
|
1806
|
+
const total = names.length;
|
|
1807
|
+
const offset = Math.max(1, Math.floor(Number(args.offset) || 1));
|
|
1808
|
+
const limit = Math.min(2000, Math.max(1, Math.floor(Number(args.limit) || 2000)));
|
|
1809
|
+
names = names.slice(offset - 1, offset - 1 + limit);
|
|
1810
|
+
const note =
|
|
1811
|
+
names.length < total
|
|
1812
|
+
? `(Showing ${names.length} of ${total} entries. Use offset parameter to paginate.)`
|
|
1813
|
+
: `(End of directory - total ${total} entries)`;
|
|
1814
|
+
return JSON.stringify({
|
|
1815
|
+
ok: true,
|
|
1816
|
+
path: abs,
|
|
1817
|
+
type: 'directory',
|
|
1818
|
+
totalEntries: total,
|
|
1819
|
+
offset,
|
|
1820
|
+
truncated: offset - 1 + names.length < total,
|
|
1821
|
+
note,
|
|
1822
|
+
content: names.join('\n'),
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
if (st.size > MAX_FILE_CHARS * 2) {
|
|
1826
|
+
return JSON.stringify({ ok: false, error: `file too large (${st.size} bytes)` });
|
|
1827
|
+
}
|
|
1828
|
+
/* binary tespiti uzantıdan — PDF'e dokunma (Beast'in pdf hattı var) */
|
|
1829
|
+
if (BINARY_EXT_RE.test(abs)) {
|
|
1830
|
+
return JSON.stringify({ ok: false, error: `Cannot read binary file: ${abs}` });
|
|
1831
|
+
}
|
|
1832
|
+
/* PDF: pdf-parse varsa metin çıkar (v2 class API — bkz. src/agent/pdf.js) */
|
|
1833
|
+
if (/\.pdf$/i.test(abs)) {
|
|
1834
|
+
let extract = null;
|
|
1835
|
+
try { extract = require('./pdf').extract; } catch {}
|
|
1836
|
+
if (!extract) return JSON.stringify({ ok: false, error: 'pdf okuma yok — npm i pdf-parse' });
|
|
1837
|
+
try {
|
|
1838
|
+
const r = await extract(fs.readFileSync(abs));
|
|
1839
|
+
let content = String((r && r.text) || '').trim();
|
|
1840
|
+
const pages = (r && r.total) || '?';
|
|
1841
|
+
const truncated = content.length > MAX_FILE_CHARS;
|
|
1842
|
+
if (truncated) content = content.slice(0, MAX_FILE_CHARS);
|
|
1843
|
+
if (!content.trim()) return JSON.stringify({ ok: false, error: 'pdf metni çıkarılamadı (taranmış görsel olabilir)' });
|
|
1844
|
+
return JSON.stringify({ ok: true, path: abs, pages, truncated, content });
|
|
1845
|
+
} catch (e2) {
|
|
1846
|
+
return JSON.stringify({ ok: false, error: 'pdf okuma hata: ' + String((e2 && e2.message) || e2) });
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
/* binary tespiti: ilk 4KB'ta NUL / %30+ yazdırılamaz karakter (opencode kuralı) */
|
|
1850
|
+
try {
|
|
1851
|
+
const fd = fs.openSync(abs, 'r');
|
|
1852
|
+
let probe;
|
|
1853
|
+
try {
|
|
1854
|
+
probe = Buffer.alloc(Math.min(4096, st.size));
|
|
1855
|
+
fs.readSync(fd, probe, 0, probe.length, 0);
|
|
1856
|
+
} finally {
|
|
1857
|
+
fs.closeSync(fd);
|
|
1858
|
+
}
|
|
1859
|
+
if (looksBinary(probe)) {
|
|
1860
|
+
return JSON.stringify({ ok: false, error: `Cannot read binary file: ${abs}` });
|
|
1861
|
+
}
|
|
1862
|
+
} catch (eProbe) {
|
|
1863
|
+
if (eProbe && /Cannot read binary/.test(String(eProbe.message || ''))) throw eProbe;
|
|
1864
|
+
}
|
|
1865
|
+
let raw;
|
|
1866
|
+
try {
|
|
1867
|
+
raw = readCacheGet(abs, st);
|
|
1868
|
+
} catch {
|
|
1869
|
+
raw = fs.readFileSync(abs, 'utf8');
|
|
1870
|
+
}
|
|
1871
|
+
/* opencode read.ts port: satır numaralı çıktı (`N: içerik`), offset/limit
|
|
1872
|
+
penceresi (1-indexed), 2000 satır varsayılan, uzun satır kırpma */
|
|
1873
|
+
const allLines = raw.split('\n');
|
|
1874
|
+
const offset = Math.max(1, Math.floor(Number(args.offset) || 1));
|
|
1875
|
+
const limit = Math.min(2000, Math.max(1, Math.floor(Number(args.limit) || 2000)));
|
|
1876
|
+
let slice = allLines
|
|
1877
|
+
.slice(offset - 1, offset - 1 + limit)
|
|
1878
|
+
.map((l, i) => {
|
|
1879
|
+
const line = l.length > 2000 ? l.slice(0, 2000) + '... (line truncated to 2000 chars)' : l;
|
|
1880
|
+
return `${offset + i}: ${line}`;
|
|
1881
|
+
});
|
|
1882
|
+
let truncated = offset - 1 + limit < allLines.length;
|
|
1883
|
+
let content = slice.join('\n');
|
|
1884
|
+
/* opencode 50KB byte tavanı: aşarsa satır bazında kes + özel footer */
|
|
1885
|
+
const MAX_BYTES = 50 * 1024;
|
|
1886
|
+
let byteCapped = false;
|
|
1887
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_BYTES) {
|
|
1888
|
+
const keep = [];
|
|
1889
|
+
let used = 0;
|
|
1890
|
+
for (const l of slice) {
|
|
1891
|
+
const need = Buffer.byteLength(l, 'utf8') + 1;
|
|
1892
|
+
if (used + need > MAX_BYTES) break;
|
|
1893
|
+
keep.push(l);
|
|
1894
|
+
used += need;
|
|
1895
|
+
}
|
|
1896
|
+
slice = keep;
|
|
1897
|
+
content = keep.join('\n');
|
|
1898
|
+
truncated = true;
|
|
1899
|
+
byteCapped = keep.length < allLines.length;
|
|
1900
|
+
}
|
|
1901
|
+
/* opencode read.ts footer'ları: byte-kesme → offset devam; satır-kesme →
|
|
1902
|
+
offset devam; aksi → dosya sonu. Model BAŞTAN okuma döngüsüne girmez. */
|
|
1903
|
+
let note;
|
|
1904
|
+
if (byteCapped && slice.length) {
|
|
1905
|
+
note = `(Output capped at 50 KB. Showing lines ${offset}-${offset + slice.length - 1}. Use offset=${offset + slice.length} to continue.)`;
|
|
1906
|
+
} else if (truncated && slice.length) {
|
|
1907
|
+
note = `(Showing lines ${offset}-${offset + slice.length - 1} of ${allLines.length}. Use offset=${offset + slice.length} to continue.)`;
|
|
1908
|
+
} else {
|
|
1909
|
+
note = `(End of file - total ${allLines.length} lines)`;
|
|
1910
|
+
}
|
|
1911
|
+
return JSON.stringify({
|
|
1912
|
+
ok: true,
|
|
1913
|
+
path: abs,
|
|
1914
|
+
totalLines: allLines.length,
|
|
1915
|
+
offset,
|
|
1916
|
+
truncated,
|
|
1917
|
+
note,
|
|
1918
|
+
content,
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
case 'write_file': {
|
|
1922
|
+
/* opencode write.ts port: üzerine yazmadan önce eski içerik alınır,
|
|
1923
|
+
sonuçta additions/deletions + UI diffView döner */
|
|
1924
|
+
const abs = safeResolve(String(args.path || ''), cwd);
|
|
1925
|
+
const content = String(args.content ?? '');
|
|
1926
|
+
const existed = fs.existsSync(abs);
|
|
1927
|
+
const before = existed && !fs.statSync(abs).isDirectory() ? fs.readFileSync(abs, 'utf8') : '';
|
|
1928
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
1929
|
+
fs.writeFileSync(abs, content, 'utf8');
|
|
1930
|
+
readCacheDrop(abs); /* sonraki read_file taze içerik okusun */
|
|
1931
|
+
const counts = diffLineCounts(before, content);
|
|
1932
|
+
const out = { ok: true, path: abs, bytes: Buffer.byteLength(content), ...counts };
|
|
1933
|
+
if (ctx.wantDiff) {
|
|
1934
|
+
out.diffView = existed
|
|
1935
|
+
? { path: abs, ...diffRegion(before, content), ...counts }
|
|
1936
|
+
: { path: abs, before: '', after: capDiffText(content), startLine: 1, ...counts };
|
|
1937
|
+
}
|
|
1938
|
+
return JSON.stringify(out);
|
|
1939
|
+
}
|
|
1940
|
+
case 'list_dir': {
|
|
1941
|
+
/* opencode read.ts dizin modu kuralı: localeCompare sıralı, klasörler
|
|
1942
|
+
`/` ile, 500 giriş tavanı + pagination notu */
|
|
1943
|
+
const abs = args.path ? safeResolve(String(args.path), cwd) : cwd;
|
|
1944
|
+
if (!fs.existsSync(abs)) return JSON.stringify({ ok: false, error: `File not found: ${abs}` });
|
|
1945
|
+
if (!fs.statSync(abs).isDirectory()) return JSON.stringify({ ok: false, error: `Path is a file, not a directory: ${abs}` });
|
|
1946
|
+
const all = fs.readdirSync(abs, { withFileTypes: true });
|
|
1947
|
+
const total = all.length;
|
|
1948
|
+
const entries = all.slice(0, 500);
|
|
1949
|
+
const rows = entries.map((e) => {
|
|
1950
|
+
try {
|
|
1951
|
+
const st = fs.statSync(path.join(abs, e.name));
|
|
1952
|
+
return `${e.isDirectory() ? 'd' : '-'} ${String(st.size).padStart(10)} ${e.isDirectory() ? e.name + '/' : e.name}`;
|
|
1953
|
+
} catch {
|
|
1954
|
+
return `- ? ${e.name}`;
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
rows.sort((a, b) => a.slice(13).localeCompare(b.slice(13)));
|
|
1958
|
+
return JSON.stringify({
|
|
1959
|
+
ok: true,
|
|
1960
|
+
path: abs,
|
|
1961
|
+
count: rows.length,
|
|
1962
|
+
...(total > 500 ? { note: `(Showing 500 of ${total} entries. Use read_file with offset to paginate.)` } : {}),
|
|
1963
|
+
entries: rows.join('\n'),
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
case 'grep': {
|
|
1967
|
+
/* opencode grep.ts BİREBİR port: case-sensitive default (rg kuralı),
|
|
1968
|
+
100 match limit, gruplu çıktı `<abs>:` + ` Line N: text`,
|
|
1969
|
+
2000 karakter satır tavanı + truncation notu */
|
|
1970
|
+
const pattern = String(args.pattern || '');
|
|
1971
|
+
let re;
|
|
1972
|
+
try {
|
|
1973
|
+
re = new RegExp(pattern, args.case_insensitive || args.caseInsensitive ? 'i' : '');
|
|
1974
|
+
} catch (e) {
|
|
1975
|
+
return JSON.stringify({ ok: false, error: 'geçersiz regex: ' + String((e && e.message) || e) });
|
|
1976
|
+
}
|
|
1977
|
+
const root = args.path ? safeResolve(String(args.path), cwd) : cwd;
|
|
1978
|
+
if (!fs.existsSync(root)) return JSON.stringify({ ok: false, error: 'yol bulunamadı: ' + root });
|
|
1979
|
+
const incRe = globToRegExp(String(args.include || ''));
|
|
1980
|
+
const hits = []; // { file, line, text }
|
|
1981
|
+
const MAX_MATCHES = 100;
|
|
1982
|
+
walkFiles(root, incRe, (file) => {
|
|
1983
|
+
if (hits.length >= MAX_MATCHES) return true; // dur
|
|
1984
|
+
let text = '';
|
|
1985
|
+
try {
|
|
1986
|
+
const st = fs.statSync(file);
|
|
1987
|
+
if (st.size > 2 * 1024 * 1024) return false; // dev dosyayı atla
|
|
1988
|
+
text = fs.readFileSync(file, 'utf8');
|
|
1989
|
+
} catch {
|
|
1990
|
+
return false;
|
|
1991
|
+
}
|
|
1992
|
+
const lines = text.split('\n');
|
|
1993
|
+
for (let i = 0; i < lines.length && hits.length < MAX_MATCHES; i++) {
|
|
1994
|
+
if (re.test(lines[i])) {
|
|
1995
|
+
const t = lines[i].slice(0, 2000) + (lines[i].length > 2000 ? '...' : '');
|
|
1996
|
+
hits.push({ file, line: i + 1, text: t });
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
return false;
|
|
2000
|
+
});
|
|
2001
|
+
/* opencode çıktı formatı: başlık + dosya gruplama + ` Line N: text` */
|
|
2002
|
+
const groups = new Map();
|
|
2003
|
+
for (const h of hits) {
|
|
2004
|
+
if (!groups.has(h.file)) groups.set(h.file, []);
|
|
2005
|
+
groups.get(h.file).push(` Line ${h.line}: ${h.text}`);
|
|
2006
|
+
}
|
|
2007
|
+
const out = hits.length
|
|
2008
|
+
? `Found ${hits.length} matches` +
|
|
2009
|
+
(hits.length >= MAX_MATCHES ? ' (more matches available)' : '') +
|
|
2010
|
+
'\n\n' +
|
|
2011
|
+
[...groups.entries()].map(([f, rows]) => `${f}:\n${rows.join('\n')}`).join('\n\n')
|
|
2012
|
+
: 'No files found';
|
|
2013
|
+
return JSON.stringify({
|
|
2014
|
+
ok: true,
|
|
2015
|
+
pattern,
|
|
2016
|
+
count: hits.length,
|
|
2017
|
+
capped: hits.length >= MAX_MATCHES,
|
|
2018
|
+
...(hits.length >= MAX_MATCHES ? { note: '(Results truncated. Consider using a more specific path or pattern.)' } : {}),
|
|
2019
|
+
matches: out,
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
case 'glob': {
|
|
2023
|
+
/* opencode glob.ts BİREBİR port: 100 sonuç limiti + truncation notu,
|
|
2024
|
+
"No files found", path-dosya hatası */
|
|
2025
|
+
const pat = String(args.pattern || '');
|
|
2026
|
+
if (!pat.trim()) return JSON.stringify({ ok: false, error: 'pattern boş olamaz' });
|
|
2027
|
+
const root = args.path ? safeResolve(String(args.path), cwd) : cwd;
|
|
2028
|
+
if (!fs.existsSync(root)) return JSON.stringify({ ok: false, error: 'yol bulunamadı: ' + root });
|
|
2029
|
+
if (fs.statSync(root).isFile()) {
|
|
2030
|
+
return JSON.stringify({ ok: false, error: `glob path must be a directory: ${root}` });
|
|
2031
|
+
}
|
|
2032
|
+
const re = globToRegExp(pat);
|
|
2033
|
+
const LIMIT = 100;
|
|
2034
|
+
const out = [];
|
|
2035
|
+
walkFiles(root, null, (file) => {
|
|
2036
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
2037
|
+
if (re.test(rel) || re.test(path.basename(file))) {
|
|
2038
|
+
out.push(path.join(root, rel));
|
|
2039
|
+
}
|
|
2040
|
+
return out.length >= LIMIT; // dur
|
|
2041
|
+
});
|
|
2042
|
+
if (!out.length) return JSON.stringify({ ok: true, pattern: pat, count: 0, files: [], note: 'No files found' });
|
|
2043
|
+
return JSON.stringify({
|
|
2044
|
+
ok: true,
|
|
2045
|
+
pattern: pat,
|
|
2046
|
+
count: out.length,
|
|
2047
|
+
files: out,
|
|
2048
|
+
...(out.length >= LIMIT ? { note: '(Results are truncated: showing first 100 results. Consider using a more specific path or pattern.)' } : {}),
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
case 'web_search': {
|
|
2052
|
+
const q = String(args.query || '');
|
|
2053
|
+
const n = Number(args.max_results) || 8;
|
|
2054
|
+
/* sıralı zincir (searxng/tinyfish/python — dahili tarayıcı engine
|
|
2055
|
+
tarafındaki hook'tan gelir); Exa KALDIRILDI */
|
|
2056
|
+
const r = await searchChainWeb(q, n, { signal: ctx.signal });
|
|
2057
|
+
return JSON.stringify(r);
|
|
2058
|
+
}
|
|
2059
|
+
case 'deep_search': {
|
|
2060
|
+
/* hook'suz bağlamda bile çalışsın: yalnız arama zinciri (sayfa okuma yok).
|
|
2061
|
+
Gizli tarayıcı okuması engine.research hook'undan gelir (main process). */
|
|
2062
|
+
const r = await research.deepSearch(
|
|
2063
|
+
args,
|
|
2064
|
+
{ search: (q) => searchChainWeb(q, 10, { signal: ctx.signal }) },
|
|
2065
|
+
ctx.signal
|
|
2066
|
+
);
|
|
2067
|
+
return JSON.stringify(r);
|
|
2068
|
+
}
|
|
2069
|
+
case 'python_run': {
|
|
2070
|
+
const t0 = Date.now();
|
|
2071
|
+
/* önce parametreler — interpreter araması gerektirmeyen hatalar erken dönsün */
|
|
2072
|
+
const cliArgs = Array.isArray(args.args)
|
|
2073
|
+
? args.args.map(String)
|
|
2074
|
+
: args.args
|
|
2075
|
+
? [String(args.args)]
|
|
2076
|
+
: [];
|
|
2077
|
+
let scriptPath;
|
|
2078
|
+
let madeTemp = false;
|
|
2079
|
+
if (String(args.code || '').trim()) {
|
|
2080
|
+
scriptPath = path.join(
|
|
2081
|
+
os.tmpdir(),
|
|
2082
|
+
`beast-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}.py`
|
|
2083
|
+
);
|
|
2084
|
+
fs.writeFileSync(scriptPath, String(args.code), 'utf8');
|
|
2085
|
+
madeTemp = true;
|
|
2086
|
+
} else if (String(args.script || '').trim()) {
|
|
2087
|
+
const raw = String(args.script);
|
|
2088
|
+
scriptPath = path.isAbsolute(raw) ? raw : path.join(pythonScriptsDir(), raw);
|
|
2089
|
+
} else {
|
|
2090
|
+
return JSON.stringify({ ok: false, error: 'code ya da script parametresi gerekli' });
|
|
2091
|
+
}
|
|
2092
|
+
if (!fs.existsSync(scriptPath)) {
|
|
2093
|
+
return JSON.stringify({
|
|
2094
|
+
ok: false,
|
|
2095
|
+
error: `script bulunamadı: ${scriptPath}`,
|
|
2096
|
+
hint: `scripts klasörü: ${pythonScriptsDir()}`,
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
let probe;
|
|
2100
|
+
try {
|
|
2101
|
+
probe = await ensurePython({ allowDownload: ctx.allowDownload !== false, signal: ctx.signal });
|
|
2102
|
+
} catch (e) {
|
|
2103
|
+
return JSON.stringify({
|
|
2104
|
+
ok: false,
|
|
2105
|
+
error: String((e && e.message) || e),
|
|
2106
|
+
hint: 'BEAST_PYTHON env ile interpreter yolu verilebilir; ya da python.org kur',
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
const r = await runPy(probe.exe, scriptPath, cliArgs, cwd, Number(args.timeout_ms) || 120000, ctx.signal);
|
|
2110
|
+
if (madeTemp) { try { fs.unlinkSync(scriptPath); } catch {} }
|
|
2111
|
+
return JSON.stringify({ ok: r.ok, python: probe.source, exitCode: r.code, ms: Date.now() - t0, output: r.output });
|
|
2112
|
+
}
|
|
2113
|
+
case 'http_fetch':
|
|
2114
|
+
case 'webfetch': {
|
|
2115
|
+
/* opencode webfetch.ts port: format (text|markdown|html) + timeout
|
|
2116
|
+
(default 30s, max 120s) — SSRF koruması Beast'te kalır */
|
|
2117
|
+
const format = String(args.format || 'text').toLowerCase();
|
|
2118
|
+
if (!['text', 'markdown', 'html'].includes(format)) {
|
|
2119
|
+
return JSON.stringify({ ok: false, error: 'format: text | markdown | html' });
|
|
2120
|
+
}
|
|
2121
|
+
const timeoutRaw = Number(args.timeout);
|
|
2122
|
+
if (args.timeout != null && (!Number.isFinite(timeoutRaw) || timeoutRaw <= 0)) {
|
|
2123
|
+
return JSON.stringify({ ok: false, error: `Invalid timeout value: ${args.timeout}. Timeout must be a positive number.` });
|
|
2124
|
+
}
|
|
2125
|
+
/* opencode saniye cinsinden bekler; ms gelirse de tolere et */
|
|
2126
|
+
let timeoutMs = 30000;
|
|
2127
|
+
if (Number.isFinite(timeoutRaw) && timeoutRaw > 0) {
|
|
2128
|
+
timeoutMs = Math.min(timeoutRaw <= 120 ? timeoutRaw * 1000 : timeoutRaw, 120000);
|
|
2129
|
+
}
|
|
2130
|
+
const r = await httpFetch(String(args.url || ''), {
|
|
2131
|
+
maxChars: Number(args.max_chars) || MAX_FETCH_CHARS,
|
|
2132
|
+
format,
|
|
2133
|
+
timeoutMs,
|
|
2134
|
+
signal: ctx.signal,
|
|
2135
|
+
});
|
|
2136
|
+
return JSON.stringify(r);
|
|
2137
|
+
}
|
|
2138
|
+
default:
|
|
2139
|
+
return JSON.stringify({ ok: false, error: `unknown tool ${name}` });
|
|
2140
|
+
}
|
|
2141
|
+
} catch (e) {
|
|
2142
|
+
return JSON.stringify({ ok: false, error: String(e && e.message ? e.message : e) });
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/* ---------- Sıralı arama zinciri (Ayarlar → Web Arama'dan değiştirilir) ----------
|
|
2147
|
+
Motorlar: searxng (yerel 8888) · browser (dahili Chromium → Google) ·
|
|
2148
|
+
DuckDuckGo) · tinyfish (API, anahtar gerekir) · python (ddgs/DDG+Bing+Mojeek).
|
|
2149
|
+
Sıra + aç/kapa ayarı settings.json'da (searchChain) saklanır; eski
|
|
2150
|
+
varsayılan AKTİF. Tarayıcı CAPTCHA/trafik verirse 10 dk atlanır. */
|
|
2151
|
+
const SEARCH_ENGINE_IDS = ['searxng', 'stealth', 'browser', 'tinyfish', 'python'];
|
|
2152
|
+
const DEFAULT_SEARCH_CHAIN = SEARCH_ENGINE_IDS.map((id) => ({ id, on: true }));
|
|
2153
|
+
|
|
2154
|
+
let _searchChain = DEFAULT_SEARCH_CHAIN.map((x) => ({ ...x }));
|
|
2155
|
+
let _browserBanUntil = 0;
|
|
2156
|
+
|
|
2157
|
+
function normalizeSearchChain(list) {
|
|
2158
|
+
const arr = Array.isArray(list) ? list : [];
|
|
2159
|
+
const rows = [];
|
|
2160
|
+
const seen = new Set();
|
|
2161
|
+
for (const item of arr) {
|
|
2162
|
+
const id = String((item && item.id) || item || '').trim();
|
|
2163
|
+
if (!SEARCH_ENGINE_IDS.includes(id) || seen.has(id)) continue;
|
|
2164
|
+
seen.add(id);
|
|
2165
|
+
rows.push({ id, on: !(item && item.on === false) });
|
|
2166
|
+
}
|
|
2167
|
+
for (const id of SEARCH_ENGINE_IDS) {
|
|
2168
|
+
if (!seen.has(id)) rows.push({ id, on: true }); /* listede eksik motor varsayılan AÇIK */
|
|
2169
|
+
}
|
|
2170
|
+
if (!rows.some((r) => r.on)) {
|
|
2171
|
+
const b = rows.find((r) => r.id === 'browser');
|
|
2172
|
+
if (b) b.on = true;
|
|
2173
|
+
}
|
|
2174
|
+
return rows;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function setSearchChain(list) {
|
|
2178
|
+
_searchChain = normalizeSearchChain(list);
|
|
2179
|
+
return _searchChain;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
|
|
2183
|
+
function getSearchChain() {
|
|
2184
|
+
return _searchChain.map((x) => ({ ...x }));
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
function browserBanned() {
|
|
2188
|
+
return Date.now() < _browserBanUntil;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function banBrowser(minutes = 10) {
|
|
2192
|
+
_browserBanUntil = Date.now() + minutes * 60 * 1000;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/* Zinciri sırayla koştur: her motor boş/hata dönerse sıradakine geç.
|
|
2196
|
+
browser motoru yalnız engine'den gelir (hook); araç-bağımsız çağrılarda atlanır. */
|
|
2197
|
+
async function searchChainWeb(query, maxResults, { signal, browser } = {}) {
|
|
2198
|
+
const q = String(query || '').trim().slice(0, 400);
|
|
2199
|
+
if (!q) return { ok: false, error: 'boş sorgu' };
|
|
2200
|
+
const cap = Math.max(1, Math.min(12, Number(maxResults) || 8));
|
|
2201
|
+
let out = null;
|
|
2202
|
+
const banned = browserBanned();
|
|
2203
|
+
/* OTOMATİK SEÇİM: yerel SearXNG ayaktaysa (kullanıcı kapatmadıysa) zincirde
|
|
2204
|
+
nerede olursa olsun ÖNE alınır — ücretsiz + CAPTCHAsız + en hızlı aday.
|
|
2205
|
+
isUp() cache'li: kapalıysa ağır maliyeti yok, normal sıra işler. */
|
|
2206
|
+
const rows = _searchChain.slice();
|
|
2207
|
+
try {
|
|
2208
|
+
const i = rows.findIndex((r) => r.id === 'searxng');
|
|
2209
|
+
if (i > 0 && rows[i].on && (await searxng.isUp())) rows.unshift(rows.splice(i, 1)[0]);
|
|
2210
|
+
} catch {}
|
|
2211
|
+
for (const row of rows) {
|
|
2212
|
+
if (out && out.ok && (out.results || []).length) break;
|
|
2213
|
+
if (!row.on) continue;
|
|
2214
|
+
try {
|
|
2215
|
+
if (row.id === 'browser') {
|
|
2216
|
+
if (typeof browser !== 'function' || banned) continue;
|
|
2217
|
+
out = await browser();
|
|
2218
|
+
if (!out || !out.ok || !(out.results || []).length) {
|
|
2219
|
+
banBrowser(); /* tarayıcı sorunlu — 10 dk atla, alternatifler devrede */
|
|
2220
|
+
out = null;
|
|
2221
|
+
}
|
|
2222
|
+
} else if (row.id === 'stealth') {
|
|
2223
|
+
/* obscura yaklaşımı: Chrome TLS taklidi (curl_cffi) */
|
|
2224
|
+
out = await stealthSearch(q, { maxResults: cap, signal });
|
|
2225
|
+
} else if (row.id === 'tinyfish') {
|
|
2226
|
+
out = await tinyfishSearch(q, cap, signal);
|
|
2227
|
+
} else if (row.id === 'searxng') {
|
|
2228
|
+
/* yerel SearXNG — ayakta değilse hızlıca atlanır (probe cache'li) */
|
|
2229
|
+
out = await searxng.search(q, { maxResults: cap, signal });
|
|
2230
|
+
} else if (row.id === 'python') {
|
|
2231
|
+
out = await webSearchFast(q, { maxResults: cap, signal });
|
|
2232
|
+
}
|
|
2233
|
+
} catch {
|
|
2234
|
+
out = null;
|
|
2235
|
+
}
|
|
2236
|
+
if (out && out.ok && !(out.results || []).length) out = null; /* boş → sıradaki motor */
|
|
2237
|
+
}
|
|
2238
|
+
if (out && out.ok && banned && (out.results || []).length) {
|
|
2239
|
+
out.note = 'dahili tarayıcı 10 dk askıda (CAPTCHA/trafik) — alternatif motor kullanıldı';
|
|
2240
|
+
}
|
|
2241
|
+
return out || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/* ---------- TinyFish (anahtar girilirse zincirdeki kendi sırasında) ----------
|
|
2245
|
+
GET https://api.search.tinyfish.ai?query=... · Header: X-API-Key
|
|
2246
|
+
Anahtar yoksa bu motor otomatik atlanır; sıradaki motor devreye girer. */
|
|
2247
|
+
let _tinyfishKey = null;
|
|
2248
|
+
|
|
2249
|
+
function setTinyfishKey(key) {
|
|
2250
|
+
_tinyfishKey = String(key || '').trim() || null;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
async function tinyfishSearch(query, maxResults, signal) {
|
|
2254
|
+
if (!_tinyfishKey) return null;
|
|
2255
|
+
try {
|
|
2256
|
+
const sig =
|
|
2257
|
+
signal && typeof AbortSignal !== 'undefined' && AbortSignal.any
|
|
2258
|
+
? AbortSignal.any([signal, AbortSignal.timeout(20000)])
|
|
2259
|
+
: AbortSignal.timeout(20000);
|
|
2260
|
+
const r = await fetch(
|
|
2261
|
+
`https://api.search.tinyfish.ai?query=${encodeURIComponent(String(query || ''))}`,
|
|
2262
|
+
{
|
|
2263
|
+
headers: {
|
|
2264
|
+
'X-API-Key': _tinyfishKey,
|
|
2265
|
+
'X-TF-Request-Origin': 'api',
|
|
2266
|
+
'X-TF-Client-Name': 'tinyfish-api-key-page',
|
|
2267
|
+
},
|
|
2268
|
+
signal: sig,
|
|
2269
|
+
}
|
|
2270
|
+
);
|
|
2271
|
+
if (!r.ok) return null;
|
|
2272
|
+
const j = await r.json();
|
|
2273
|
+
const results = (j.results || [])
|
|
2274
|
+
.map((x) => ({
|
|
2275
|
+
title: String(x.title || ''),
|
|
2276
|
+
url: String(x.url || ''),
|
|
2277
|
+
snippet: String(x.snippet || '').replace(/\s+/g, ' ').slice(0, 400),
|
|
2278
|
+
engine: 'tinyfish',
|
|
2279
|
+
}))
|
|
2280
|
+
.filter((x) => x.url && x.title);
|
|
2281
|
+
if (!results.length) return null;
|
|
2282
|
+
return { ok: true, engine: 'tinyfish', query, results: results.slice(0, Math.max(1, Math.min(12, Number(maxResults) || 8))) };
|
|
2283
|
+
} catch {
|
|
2284
|
+
return null;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
module.exports = {
|
|
2289
|
+
definitions,
|
|
2290
|
+
exec,
|
|
2291
|
+
/* opencode edit.ts portu (test + dış kullanım) */
|
|
2292
|
+
ocReplace,
|
|
2293
|
+
diffLineCounts,
|
|
2294
|
+
diffRegion,
|
|
2295
|
+
ocReplacers: {
|
|
2296
|
+
SimpleReplacer,
|
|
2297
|
+
LineTrimmedReplacer,
|
|
2298
|
+
BlockAnchorReplacer,
|
|
2299
|
+
WhitespaceNormalizedReplacer,
|
|
2300
|
+
IndentationFlexibleReplacer,
|
|
2301
|
+
EscapeNormalizedReplacer,
|
|
2302
|
+
TrimmedBoundaryReplacer,
|
|
2303
|
+
ContextAwareReplacer,
|
|
2304
|
+
MultiOccurrenceReplacer,
|
|
2305
|
+
},
|
|
2306
|
+
runCommand,
|
|
2307
|
+
runShellCommand,
|
|
2308
|
+
runBashCommand,
|
|
2309
|
+
gitbash,
|
|
2310
|
+
disposeShellSessions,
|
|
2311
|
+
boundToolOutput,
|
|
2312
|
+
globToRegExp,
|
|
2313
|
+
walkFiles,
|
|
2314
|
+
assertPublicHttpUrl,
|
|
2315
|
+
parseDdgResults,
|
|
2316
|
+
htmlToText,
|
|
2317
|
+
webSearch,
|
|
2318
|
+
httpFetch,
|
|
2319
|
+
webSearchFast,
|
|
2320
|
+
seedScript,
|
|
2321
|
+
bundledScriptPath,
|
|
2322
|
+
/* arama zinciri (sıra + aç/kapa Ayarlar → Web Arama'dan) */
|
|
2323
|
+
SEARCH_ENGINE_IDS,
|
|
2324
|
+
DEFAULT_SEARCH_CHAIN,
|
|
2325
|
+
setSearchChain,
|
|
2326
|
+
getSearchChain,
|
|
2327
|
+
searchChainWeb,
|
|
2328
|
+
browserBanned,
|
|
2329
|
+
banBrowser,
|
|
2330
|
+
setTinyfishKey,
|
|
2331
|
+
tinyfishSearch,
|
|
2332
|
+
stealthSearch,
|
|
2333
|
+
/* python altyapısı */
|
|
2334
|
+
ensurePython,
|
|
2335
|
+
findSystemPython,
|
|
2336
|
+
installEmbeddedPython,
|
|
2337
|
+
pythonScriptsDir,
|
|
2338
|
+
embeddedPythonExe,
|
|
2339
|
+
runPy,
|
|
2340
|
+
};
|