beast-agent 1.6.0 → 1.7.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/README.md +7 -0
- package/package.json +2 -1
- package/scripts/fix-electron.js +12 -0
- package/src/agent/bots.js +13 -0
- package/src/agent/engine.js +4962 -4906
- package/src/agent/mem0.js +605 -0
- package/src/agent/memory.js +8 -0
- package/src/agent/tools.js +278 -37
- package/src/main.js +32 -18
- package/src/preload.js +1 -0
- package/src/renderer/i18n.js +4 -0
- package/src/renderer/index.html +5 -0
- package/src/renderer/renderer.js +72 -13
- package/src/renderer/style.css +38 -2
package/src/agent/tools.js
CHANGED
|
@@ -364,6 +364,49 @@ function safeResolve(p, cwd) {
|
|
|
364
364
|
return path.normalize(abs);
|
|
365
365
|
}
|
|
366
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
|
+
|
|
367
410
|
/* ---------- opencode port: grep/glob altyapısı ----------
|
|
368
411
|
ripgrep gitignore'u izler; Beast'te bağımlılık olmasın diye ağır klasörler
|
|
369
412
|
statik atlanır (node_modules, .git, derleme çıktıları…). */
|
|
@@ -788,6 +831,27 @@ function htmlToText(html) {
|
|
|
788
831
|
return t.trim();
|
|
789
832
|
}
|
|
790
833
|
|
|
834
|
+
/* opencode webfetch.ts tarzı HTML→Markdown (hafif sürüm — Turndown bağımlılığı yok):
|
|
835
|
+
başlıklar, linkler, kalık/italik, kod blokları, listeler korunur */
|
|
836
|
+
function htmlToMarkdown(html) {
|
|
837
|
+
let h = String(html || '');
|
|
838
|
+
h = h.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<!--[\s\S]*?-->/g, '');
|
|
839
|
+
h = h.replace(/<pre[^>]*>\s*<code[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_m, c) => '\n```\n' + decodeEntities(c) + '\n```\n');
|
|
840
|
+
h = h.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_m, c) => '`' + decodeEntities(c) + '`');
|
|
841
|
+
h = h.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, lvl, c) => '\n' + '#'.repeat(Number(lvl)) + ' ' + decodeEntities(c.replace(/<[^>]+>/g, '')).trim() + '\n');
|
|
842
|
+
h = h.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_m, href, c) => `[${decodeEntities(c.replace(/<[^>]+>/g, '')).trim()}](${href})`);
|
|
843
|
+
h = h.replace(/<(b|strong)[^>]*>([\s\S]*?)<\/\1>/gi, (_m, _t, c) => `**${decodeEntities(c.replace(/<[^>]+>/g, '')).trim()}**`);
|
|
844
|
+
h = h.replace(/<(i|em)[^>]*>([\s\S]*?)<\/\1>/gi, (_m, _t, c) => `*${decodeEntities(c.replace(/<[^>]+>/g, '')).trim()}*`);
|
|
845
|
+
h = h.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_m, c) => `\n- ${decodeEntities(c.replace(/<[^>]+>/g, '')).trim()}`);
|
|
846
|
+
h = h.replace(/<hr\s*\/?>/gi, '\n---\n');
|
|
847
|
+
h = h.replace(/<br\s*\/?>/gi, '\n');
|
|
848
|
+
h = h.replace(/<\/(p|div|section|article|tr|h[1-6])>/gi, '\n');
|
|
849
|
+
h = h.replace(/<[^>]+>/g, '');
|
|
850
|
+
h = decodeEntities(h);
|
|
851
|
+
h = h.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n');
|
|
852
|
+
return h.trim();
|
|
853
|
+
}
|
|
854
|
+
|
|
791
855
|
/* DuckDuckGo HTML sonuç ayrıştırıcı — test edilebilir saf fonksiyon */
|
|
792
856
|
function parseDdgResults(html, limit = 8) {
|
|
793
857
|
const out = [];
|
|
@@ -864,12 +928,13 @@ async function webSearch(query, { maxResults = 8, signal } = {}) {
|
|
|
864
928
|
|
|
865
929
|
const MAX_FETCH_CHARS = 9000;
|
|
866
930
|
|
|
867
|
-
async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, signal } = {}) {
|
|
931
|
+
async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, format = 'text', timeoutMs = 30000, signal } = {}) {
|
|
868
932
|
const safe = assertPublicHttpUrl(url);
|
|
933
|
+
const t = Math.min(Math.max(Number(timeoutMs) || 30000, 1000), 120000); // opencode: default 30s, max 120s
|
|
869
934
|
const res = await fetchWithTimeout(
|
|
870
935
|
safe,
|
|
871
936
|
{ headers: { 'User-Agent': UA, Accept: 'text/html,text/plain,application/json;q=0.9,*/*;q=0.5' } },
|
|
872
|
-
|
|
937
|
+
t,
|
|
873
938
|
signal
|
|
874
939
|
);
|
|
875
940
|
const ctype = String(res.headers.get('content-type') || '');
|
|
@@ -882,15 +947,23 @@ async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, signal } = {}) {
|
|
|
882
947
|
const cap = 400000;
|
|
883
948
|
let body = await res.text();
|
|
884
949
|
if (body.length > cap) body = body.slice(0, cap);
|
|
885
|
-
|
|
886
|
-
|
|
950
|
+
let content;
|
|
951
|
+
if (/html/i.test(ctype)) {
|
|
952
|
+
if (format === 'html') content = body;
|
|
953
|
+
else if (format === 'markdown') content = htmlToMarkdown(body);
|
|
954
|
+
else content = htmlToText(body);
|
|
955
|
+
} else {
|
|
956
|
+
content = body;
|
|
957
|
+
}
|
|
958
|
+
const truncated = content.length > maxChars;
|
|
887
959
|
return {
|
|
888
960
|
ok: true,
|
|
889
961
|
url: safe,
|
|
890
962
|
status: res.status,
|
|
891
963
|
contentType: ctype,
|
|
964
|
+
format,
|
|
892
965
|
truncated,
|
|
893
|
-
content:
|
|
966
|
+
content: content.slice(0, Math.max(1000, Math.min(Number(maxChars) || MAX_FETCH_CHARS, 50000))),
|
|
894
967
|
};
|
|
895
968
|
}
|
|
896
969
|
|
|
@@ -1332,7 +1405,7 @@ const definitions = [
|
|
|
1332
1405
|
type: 'object',
|
|
1333
1406
|
properties: {
|
|
1334
1407
|
command: { type: 'string', description: 'PowerShell command line to execute' },
|
|
1335
|
-
timeout_ms: { type: 'number', description: 'Optional timeout in ms (default
|
|
1408
|
+
timeout_ms: { type: 'number', description: 'Optional timeout in ms (default 120000)' },
|
|
1336
1409
|
},
|
|
1337
1410
|
required: ['command'],
|
|
1338
1411
|
},
|
|
@@ -1422,13 +1495,14 @@ const definitions = [
|
|
|
1422
1495
|
function: {
|
|
1423
1496
|
name: 'grep',
|
|
1424
1497
|
description:
|
|
1425
|
-
'Fast content search tool that works with any codebase size. Searches file contents using regular expressions; supports full regex syntax (eg. "log.*Error", "function\\s+\\w+"). Filter files by pattern with include (eg. "*.js", "*.{ts,tsx}"). Returns
|
|
1498
|
+
'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.',
|
|
1426
1499
|
parameters: {
|
|
1427
1500
|
type: 'object',
|
|
1428
1501
|
properties: {
|
|
1429
1502
|
pattern: { type: 'string', description: 'Regular expression' },
|
|
1430
1503
|
path: { type: 'string', description: 'File or directory to search; defaults to workspace root' },
|
|
1431
1504
|
include: { type: 'string', description: 'Glob filter like "*.js" or "*.{ts,tsx}"' },
|
|
1505
|
+
case_insensitive: { type: 'boolean', description: 'Ignore case (default false — search is case-sensitive)' },
|
|
1432
1506
|
},
|
|
1433
1507
|
required: ['pattern'],
|
|
1434
1508
|
},
|
|
@@ -1439,7 +1513,7 @@ const definitions = [
|
|
|
1439
1513
|
function: {
|
|
1440
1514
|
name: 'glob',
|
|
1441
1515
|
description:
|
|
1442
|
-
'Fast file pattern matching tool that works with any codebase size. Supports glob patterns like "**/*.js" or "src/**/*.ts". Returns matching file paths. Use when you need to find files by name patterns; batch multiple speculative searches in one turn.',
|
|
1516
|
+
'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.',
|
|
1443
1517
|
parameters: {
|
|
1444
1518
|
type: 'object',
|
|
1445
1519
|
properties: {
|
|
@@ -1454,7 +1528,7 @@ const definitions = [
|
|
|
1454
1528
|
type: 'function',
|
|
1455
1529
|
function: {
|
|
1456
1530
|
name: 'list_dir',
|
|
1457
|
-
description: 'List entries of a directory with sizes and types.',
|
|
1531
|
+
description: 'List entries of a directory (localeCompare sorted, directories suffixed with `/`, max 500 entries) with sizes and types.',
|
|
1458
1532
|
parameters: {
|
|
1459
1533
|
type: 'object',
|
|
1460
1534
|
properties: {
|
|
@@ -1464,6 +1538,33 @@ const definitions = [
|
|
|
1464
1538
|
},
|
|
1465
1539
|
},
|
|
1466
1540
|
},
|
|
1541
|
+
{
|
|
1542
|
+
type: 'function',
|
|
1543
|
+
function: {
|
|
1544
|
+
name: 'webfetch',
|
|
1545
|
+
description:
|
|
1546
|
+
'- Fetches content from a specified URL\n' +
|
|
1547
|
+
'- Takes a URL and optional format as input\n' +
|
|
1548
|
+
'- Fetches the URL content, converts to requested format (markdown by default)\n' +
|
|
1549
|
+
'- Returns the content in the specified format\n' +
|
|
1550
|
+
'- Use this tool when you need to retrieve and analyze web content\n' +
|
|
1551
|
+
'- 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' +
|
|
1552
|
+
'- The URL must be a fully-formed valid URL\n' +
|
|
1553
|
+
'- HTTP URLs will be automatically upgraded to HTTPS\n' +
|
|
1554
|
+
'- Format options: "text" (default), "markdown", or "html"\n' +
|
|
1555
|
+
'- Local/private network addresses are blocked; timeout default 30s (max 120s)',
|
|
1556
|
+
parameters: {
|
|
1557
|
+
type: 'object',
|
|
1558
|
+
properties: {
|
|
1559
|
+
url: { type: 'string', format: 'uri', description: 'The URL to fetch content from' },
|
|
1560
|
+
format: { type: 'string', enum: ['text', 'markdown', 'html'], description: 'The format to return the content in (defaults to text)' },
|
|
1561
|
+
timeout: { type: 'number', description: 'Optional timeout in seconds (max 120)' },
|
|
1562
|
+
max_chars: { type: 'number', description: 'Output character cap (default 9000, max 50000)' },
|
|
1563
|
+
},
|
|
1564
|
+
required: ['url'],
|
|
1565
|
+
},
|
|
1566
|
+
},
|
|
1567
|
+
},
|
|
1467
1568
|
{
|
|
1468
1569
|
type: 'function',
|
|
1469
1570
|
function: {
|
|
@@ -1546,7 +1647,12 @@ async function exec(name, args, ctx) {
|
|
|
1546
1647
|
try {
|
|
1547
1648
|
switch (name) {
|
|
1548
1649
|
case 'run_command': {
|
|
1549
|
-
|
|
1650
|
+
/* opencode shell.ts kuralları: default 120s, negatif timeout reddi */
|
|
1651
|
+
const tRaw = Number(args.timeout_ms);
|
|
1652
|
+
if (Number.isFinite(tRaw) && args.timeout_ms != null && tRaw <= 0) {
|
|
1653
|
+
return JSON.stringify({ ok: false, error: `Invalid timeout value: ${tRaw}. Timeout must be a positive number.` });
|
|
1654
|
+
}
|
|
1655
|
+
const t = Number.isFinite(tRaw) && tRaw > 0 ? tRaw : 120000;
|
|
1550
1656
|
const shell = String(args.shell || 'powershell').toLowerCase();
|
|
1551
1657
|
const r =
|
|
1552
1658
|
shell === 'bash' || shell === 'sh'
|
|
@@ -1556,7 +1662,7 @@ async function exec(name, args, ctx) {
|
|
|
1556
1662
|
return JSON.stringify({
|
|
1557
1663
|
ok: !!(r && r.ok),
|
|
1558
1664
|
code: r && r.code,
|
|
1559
|
-
output: b.text,
|
|
1665
|
+
output: b.text || '(no output)',
|
|
1560
1666
|
...(b.outputFile ? { outputFile: b.outputFile } : {}),
|
|
1561
1667
|
});
|
|
1562
1668
|
}
|
|
@@ -1613,11 +1719,53 @@ async function exec(name, args, ctx) {
|
|
|
1613
1719
|
}
|
|
1614
1720
|
}
|
|
1615
1721
|
case 'read_file': {
|
|
1722
|
+
/* opencode read.ts BİREBİR port: fuzzy not-found, binary tespiti,
|
|
1723
|
+
dizin okuma, `N: içerik` formatı, 2000 satır / 2000 karakter /
|
|
1724
|
+
50KB tavanları + opencode devam footer'ları */
|
|
1616
1725
|
const abs = safeResolve(String(args.path || ''), cwd);
|
|
1726
|
+
if (!fs.existsSync(abs)) {
|
|
1727
|
+
const sibs = fuzzySiblings(abs);
|
|
1728
|
+
const hint = sibs.length ? '\nDid you mean one of these?\n' + sibs.map((s) => `- ${path.join(path.dirname(abs), s)}`).join('\n') : '';
|
|
1729
|
+
return JSON.stringify({ ok: false, error: `File not found: ${abs}${hint}` });
|
|
1730
|
+
}
|
|
1617
1731
|
const st = fs.statSync(abs);
|
|
1732
|
+
/* DİZİN OKUMA (opencode read.ts dizin modu): localeCompare sıralı,
|
|
1733
|
+
klasörler `/` ile, offset/limit sayfalı */
|
|
1734
|
+
if (st.isDirectory()) {
|
|
1735
|
+
let names = fs
|
|
1736
|
+
.readdirSync(abs)
|
|
1737
|
+
.sort((a, b) => a.localeCompare(b))
|
|
1738
|
+
.map((name) => {
|
|
1739
|
+
let isDir = false;
|
|
1740
|
+
try { isDir = fs.statSync(path.join(abs, name)).isDirectory(); } catch {}
|
|
1741
|
+
return isDir ? name + '/' : name;
|
|
1742
|
+
});
|
|
1743
|
+
const total = names.length;
|
|
1744
|
+
const offset = Math.max(1, Math.floor(Number(args.offset) || 1));
|
|
1745
|
+
const limit = Math.min(2000, Math.max(1, Math.floor(Number(args.limit) || 2000)));
|
|
1746
|
+
names = names.slice(offset - 1, offset - 1 + limit);
|
|
1747
|
+
const note =
|
|
1748
|
+
names.length < total
|
|
1749
|
+
? `(Showing ${names.length} of ${total} entries. Use offset parameter to paginate.)`
|
|
1750
|
+
: `(End of directory - total ${total} entries)`;
|
|
1751
|
+
return JSON.stringify({
|
|
1752
|
+
ok: true,
|
|
1753
|
+
path: abs,
|
|
1754
|
+
type: 'directory',
|
|
1755
|
+
totalEntries: total,
|
|
1756
|
+
offset,
|
|
1757
|
+
truncated: offset - 1 + names.length < total,
|
|
1758
|
+
note,
|
|
1759
|
+
content: names.join('\n'),
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1618
1762
|
if (st.size > MAX_FILE_CHARS * 2) {
|
|
1619
1763
|
return JSON.stringify({ ok: false, error: `file too large (${st.size} bytes)` });
|
|
1620
1764
|
}
|
|
1765
|
+
/* binary tespiti uzantıdan — PDF'e dokunma (Beast'in pdf hattı var) */
|
|
1766
|
+
if (BINARY_EXT_RE.test(abs)) {
|
|
1767
|
+
return JSON.stringify({ ok: false, error: `Cannot read binary file: ${abs}` });
|
|
1768
|
+
}
|
|
1621
1769
|
/* PDF: pdf-parse varsa metin çıkar (v2 class API — bkz. src/agent/pdf.js) */
|
|
1622
1770
|
if (/\.pdf$/i.test(abs)) {
|
|
1623
1771
|
let extract = null;
|
|
@@ -1635,7 +1783,28 @@ async function exec(name, args, ctx) {
|
|
|
1635
1783
|
return JSON.stringify({ ok: false, error: 'pdf okuma hata: ' + String((e2 && e2.message) || e2) });
|
|
1636
1784
|
}
|
|
1637
1785
|
}
|
|
1638
|
-
|
|
1786
|
+
/* binary tespiti: ilk 4KB'ta NUL / %30+ yazdırılamaz karakter (opencode kuralı) */
|
|
1787
|
+
try {
|
|
1788
|
+
const fd = fs.openSync(abs, 'r');
|
|
1789
|
+
let probe;
|
|
1790
|
+
try {
|
|
1791
|
+
probe = Buffer.alloc(Math.min(4096, st.size));
|
|
1792
|
+
fs.readSync(fd, probe, 0, probe.length, 0);
|
|
1793
|
+
} finally {
|
|
1794
|
+
fs.closeSync(fd);
|
|
1795
|
+
}
|
|
1796
|
+
if (looksBinary(probe)) {
|
|
1797
|
+
return JSON.stringify({ ok: false, error: `Cannot read binary file: ${abs}` });
|
|
1798
|
+
}
|
|
1799
|
+
} catch (eProbe) {
|
|
1800
|
+
if (eProbe && /Cannot read binary/.test(String(eProbe.message || ''))) throw eProbe;
|
|
1801
|
+
}
|
|
1802
|
+
let raw;
|
|
1803
|
+
try {
|
|
1804
|
+
raw = readCacheGet(abs, st);
|
|
1805
|
+
} catch {
|
|
1806
|
+
raw = fs.readFileSync(abs, 'utf8');
|
|
1807
|
+
}
|
|
1639
1808
|
/* opencode read.ts port: satır numaralı çıktı (`N: içerik`), offset/limit
|
|
1640
1809
|
penceresi (1-indexed), 2000 satır varsayılan, uzun satır kırpma */
|
|
1641
1810
|
const allLines = raw.split('\n');
|
|
@@ -1644,22 +1813,37 @@ async function exec(name, args, ctx) {
|
|
|
1644
1813
|
let slice = allLines
|
|
1645
1814
|
.slice(offset - 1, offset - 1 + limit)
|
|
1646
1815
|
.map((l, i) => {
|
|
1647
|
-
const line = l.length > 2000 ? l.slice(0, 2000) + '
|
|
1816
|
+
const line = l.length > 2000 ? l.slice(0, 2000) + '... (line truncated to 2000 chars)' : l;
|
|
1648
1817
|
return `${offset + i}: ${line}`;
|
|
1649
1818
|
});
|
|
1650
1819
|
let truncated = offset - 1 + limit < allLines.length;
|
|
1651
1820
|
let content = slice.join('\n');
|
|
1652
|
-
/* byte tavanı:
|
|
1653
|
-
|
|
1821
|
+
/* opencode 50KB byte tavanı: aşarsa satır bazında kes + özel footer */
|
|
1822
|
+
const MAX_BYTES = 50 * 1024;
|
|
1823
|
+
let byteCapped = false;
|
|
1824
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_BYTES) {
|
|
1654
1825
|
const keep = [];
|
|
1655
1826
|
let used = 0;
|
|
1656
1827
|
for (const l of slice) {
|
|
1657
|
-
|
|
1828
|
+
const need = Buffer.byteLength(l, 'utf8') + 1;
|
|
1829
|
+
if (used + need > MAX_BYTES) break;
|
|
1658
1830
|
keep.push(l);
|
|
1659
|
-
used +=
|
|
1831
|
+
used += need;
|
|
1660
1832
|
}
|
|
1833
|
+
slice = keep;
|
|
1661
1834
|
content = keep.join('\n');
|
|
1662
1835
|
truncated = true;
|
|
1836
|
+
byteCapped = keep.length < allLines.length;
|
|
1837
|
+
}
|
|
1838
|
+
/* opencode read.ts footer'ları: byte-kesme → offset devam; satır-kesme →
|
|
1839
|
+
offset devam; aksi → dosya sonu. Model BAŞTAN okuma döngüsüne girmez. */
|
|
1840
|
+
let note;
|
|
1841
|
+
if (byteCapped && slice.length) {
|
|
1842
|
+
note = `(Output capped at 50 KB. Showing lines ${offset}-${offset + slice.length - 1}. Use offset=${offset + slice.length} to continue.)`;
|
|
1843
|
+
} else if (truncated && slice.length) {
|
|
1844
|
+
note = `(Showing lines ${offset}-${offset + slice.length - 1} of ${allLines.length}. Use offset=${offset + slice.length} to continue.)`;
|
|
1845
|
+
} else {
|
|
1846
|
+
note = `(End of file - total ${allLines.length} lines)`;
|
|
1663
1847
|
}
|
|
1664
1848
|
return JSON.stringify({
|
|
1665
1849
|
ok: true,
|
|
@@ -1667,11 +1851,7 @@ async function exec(name, args, ctx) {
|
|
|
1667
1851
|
totalLines: allLines.length,
|
|
1668
1852
|
offset,
|
|
1669
1853
|
truncated,
|
|
1670
|
-
|
|
1671
|
-
okur — BAŞTAN okuma döngüsü kırılır */
|
|
1672
|
-
...(truncated
|
|
1673
|
-
? { note: `(Devam ediyor — toplam ${allLines.length} satır. Kalan bölüm için offset:${offset + slice.length} ile oku; dosyayı BAŞTAN okuma.)` }
|
|
1674
|
-
: { note: `(End of file - total ${allLines.length} lines)` }),
|
|
1854
|
+
note,
|
|
1675
1855
|
content,
|
|
1676
1856
|
});
|
|
1677
1857
|
}
|
|
@@ -1695,33 +1875,49 @@ async function exec(name, args, ctx) {
|
|
|
1695
1875
|
return JSON.stringify(out);
|
|
1696
1876
|
}
|
|
1697
1877
|
case 'list_dir': {
|
|
1878
|
+
/* opencode read.ts dizin modu kuralı: localeCompare sıralı, klasörler
|
|
1879
|
+
`/` ile, 500 giriş tavanı + pagination notu */
|
|
1698
1880
|
const abs = args.path ? safeResolve(String(args.path), cwd) : cwd;
|
|
1699
|
-
|
|
1881
|
+
if (!fs.existsSync(abs)) return JSON.stringify({ ok: false, error: `File not found: ${abs}` });
|
|
1882
|
+
if (!fs.statSync(abs).isDirectory()) return JSON.stringify({ ok: false, error: `Path is a file, not a directory: ${abs}` });
|
|
1883
|
+
const all = fs.readdirSync(abs, { withFileTypes: true });
|
|
1884
|
+
const total = all.length;
|
|
1885
|
+
const entries = all.slice(0, 500);
|
|
1700
1886
|
const rows = entries.map((e) => {
|
|
1701
1887
|
try {
|
|
1702
1888
|
const st = fs.statSync(path.join(abs, e.name));
|
|
1703
|
-
return `${e.isDirectory() ? 'd' : '-'} ${String(st.size).padStart(10)} ${e.name}`;
|
|
1889
|
+
return `${e.isDirectory() ? 'd' : '-'} ${String(st.size).padStart(10)} ${e.isDirectory() ? e.name + '/' : e.name}`;
|
|
1704
1890
|
} catch {
|
|
1705
1891
|
return `- ? ${e.name}`;
|
|
1706
1892
|
}
|
|
1707
1893
|
});
|
|
1708
|
-
|
|
1894
|
+
rows.sort((a, b) => a.slice(13).localeCompare(b.slice(13)));
|
|
1895
|
+
return JSON.stringify({
|
|
1896
|
+
ok: true,
|
|
1897
|
+
path: abs,
|
|
1898
|
+
count: rows.length,
|
|
1899
|
+
...(total > 500 ? { note: `(Showing 500 of ${total} entries. Use read_file with offset to paginate.)` } : {}),
|
|
1900
|
+
entries: rows.join('\n'),
|
|
1901
|
+
});
|
|
1709
1902
|
}
|
|
1710
1903
|
case 'grep': {
|
|
1904
|
+
/* opencode grep.ts BİREBİR port: case-sensitive default (rg kuralı),
|
|
1905
|
+
100 match limit, gruplu çıktı `<abs>:` + ` Line N: text`,
|
|
1906
|
+
2000 karakter satır tavanı + truncation notu */
|
|
1711
1907
|
const pattern = String(args.pattern || '');
|
|
1712
1908
|
let re;
|
|
1713
1909
|
try {
|
|
1714
|
-
re = new RegExp(pattern, 'i');
|
|
1910
|
+
re = new RegExp(pattern, args.case_insensitive || args.caseInsensitive ? 'i' : '');
|
|
1715
1911
|
} catch (e) {
|
|
1716
1912
|
return JSON.stringify({ ok: false, error: 'geçersiz regex: ' + String((e && e.message) || e) });
|
|
1717
1913
|
}
|
|
1718
1914
|
const root = args.path ? safeResolve(String(args.path), cwd) : cwd;
|
|
1719
1915
|
if (!fs.existsSync(root)) return JSON.stringify({ ok: false, error: 'yol bulunamadı: ' + root });
|
|
1720
1916
|
const incRe = globToRegExp(String(args.include || ''));
|
|
1721
|
-
const
|
|
1722
|
-
const MAX_MATCHES =
|
|
1917
|
+
const hits = []; // { file, line, text }
|
|
1918
|
+
const MAX_MATCHES = 100;
|
|
1723
1919
|
walkFiles(root, incRe, (file) => {
|
|
1724
|
-
if (
|
|
1920
|
+
if (hits.length >= MAX_MATCHES) return true; // dur
|
|
1725
1921
|
let text = '';
|
|
1726
1922
|
try {
|
|
1727
1923
|
const st = fs.statSync(file);
|
|
@@ -1731,36 +1927,63 @@ async function exec(name, args, ctx) {
|
|
|
1731
1927
|
return false;
|
|
1732
1928
|
}
|
|
1733
1929
|
const lines = text.split('\n');
|
|
1734
|
-
for (let i = 0; i < lines.length &&
|
|
1930
|
+
for (let i = 0; i < lines.length && hits.length < MAX_MATCHES; i++) {
|
|
1735
1931
|
if (re.test(lines[i])) {
|
|
1736
|
-
|
|
1932
|
+
const t = lines[i].slice(0, 2000) + (lines[i].length > 2000 ? '...' : '');
|
|
1933
|
+
hits.push({ file, line: i + 1, text: t });
|
|
1737
1934
|
}
|
|
1738
1935
|
}
|
|
1739
1936
|
return false;
|
|
1740
1937
|
});
|
|
1938
|
+
/* opencode çıktı formatı: başlık + dosya gruplama + ` Line N: text` */
|
|
1939
|
+
const groups = new Map();
|
|
1940
|
+
for (const h of hits) {
|
|
1941
|
+
if (!groups.has(h.file)) groups.set(h.file, []);
|
|
1942
|
+
groups.get(h.file).push(` Line ${h.line}: ${h.text}`);
|
|
1943
|
+
}
|
|
1944
|
+
const out = hits.length
|
|
1945
|
+
? `Found ${hits.length} matches` +
|
|
1946
|
+
(hits.length >= MAX_MATCHES ? ' (more matches available)' : '') +
|
|
1947
|
+
'\n\n' +
|
|
1948
|
+
[...groups.entries()].map(([f, rows]) => `${f}:\n${rows.join('\n')}`).join('\n\n')
|
|
1949
|
+
: 'No files found';
|
|
1741
1950
|
return JSON.stringify({
|
|
1742
1951
|
ok: true,
|
|
1743
1952
|
pattern,
|
|
1744
|
-
count:
|
|
1745
|
-
capped:
|
|
1746
|
-
|
|
1953
|
+
count: hits.length,
|
|
1954
|
+
capped: hits.length >= MAX_MATCHES,
|
|
1955
|
+
...(hits.length >= MAX_MATCHES ? { note: '(Results truncated. Consider using a more specific path or pattern.)' } : {}),
|
|
1956
|
+
matches: out,
|
|
1747
1957
|
});
|
|
1748
1958
|
}
|
|
1749
1959
|
case 'glob': {
|
|
1960
|
+
/* opencode glob.ts BİREBİR port: 100 sonuç limiti + truncation notu,
|
|
1961
|
+
"No files found", path-dosya hatası */
|
|
1750
1962
|
const pat = String(args.pattern || '');
|
|
1751
1963
|
if (!pat.trim()) return JSON.stringify({ ok: false, error: 'pattern boş olamaz' });
|
|
1752
1964
|
const root = args.path ? safeResolve(String(args.path), cwd) : cwd;
|
|
1753
1965
|
if (!fs.existsSync(root)) return JSON.stringify({ ok: false, error: 'yol bulunamadı: ' + root });
|
|
1966
|
+
if (fs.statSync(root).isFile()) {
|
|
1967
|
+
return JSON.stringify({ ok: false, error: `glob path must be a directory: ${root}` });
|
|
1968
|
+
}
|
|
1754
1969
|
const re = globToRegExp(pat);
|
|
1970
|
+
const LIMIT = 100;
|
|
1755
1971
|
const out = [];
|
|
1756
1972
|
walkFiles(root, null, (file) => {
|
|
1757
1973
|
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
1758
1974
|
if (re.test(rel) || re.test(path.basename(file))) {
|
|
1759
1975
|
out.push(path.join(root, rel));
|
|
1760
1976
|
}
|
|
1761
|
-
return out.length >=
|
|
1977
|
+
return out.length >= LIMIT; // dur
|
|
1978
|
+
});
|
|
1979
|
+
if (!out.length) return JSON.stringify({ ok: true, pattern: pat, count: 0, files: [], note: 'No files found' });
|
|
1980
|
+
return JSON.stringify({
|
|
1981
|
+
ok: true,
|
|
1982
|
+
pattern: pat,
|
|
1983
|
+
count: out.length,
|
|
1984
|
+
files: out,
|
|
1985
|
+
...(out.length >= LIMIT ? { note: '(Results are truncated: showing first 100 results. Consider using a more specific path or pattern.)' } : {}),
|
|
1762
1986
|
});
|
|
1763
|
-
return JSON.stringify({ ok: true, pattern: pat, count: out.length, files: out });
|
|
1764
1987
|
}
|
|
1765
1988
|
case 'web_search': {
|
|
1766
1989
|
const q = String(args.query || '');
|
|
@@ -1824,9 +2047,27 @@ async function exec(name, args, ctx) {
|
|
|
1824
2047
|
if (madeTemp) { try { fs.unlinkSync(scriptPath); } catch {} }
|
|
1825
2048
|
return JSON.stringify({ ok: r.ok, python: probe.source, exitCode: r.code, ms: Date.now() - t0, output: r.output });
|
|
1826
2049
|
}
|
|
1827
|
-
case 'http_fetch':
|
|
2050
|
+
case 'http_fetch':
|
|
2051
|
+
case 'webfetch': {
|
|
2052
|
+
/* opencode webfetch.ts port: format (text|markdown|html) + timeout
|
|
2053
|
+
(default 30s, max 120s) — SSRF koruması Beast'te kalır */
|
|
2054
|
+
const format = String(args.format || 'text').toLowerCase();
|
|
2055
|
+
if (!['text', 'markdown', 'html'].includes(format)) {
|
|
2056
|
+
return JSON.stringify({ ok: false, error: 'format: text | markdown | html' });
|
|
2057
|
+
}
|
|
2058
|
+
const timeoutRaw = Number(args.timeout);
|
|
2059
|
+
if (args.timeout != null && (!Number.isFinite(timeoutRaw) || timeoutRaw <= 0)) {
|
|
2060
|
+
return JSON.stringify({ ok: false, error: `Invalid timeout value: ${args.timeout}. Timeout must be a positive number.` });
|
|
2061
|
+
}
|
|
2062
|
+
/* opencode saniye cinsinden bekler; ms gelirse de tolere et */
|
|
2063
|
+
let timeoutMs = 30000;
|
|
2064
|
+
if (Number.isFinite(timeoutRaw) && timeoutRaw > 0) {
|
|
2065
|
+
timeoutMs = Math.min(timeoutRaw <= 120 ? timeoutRaw * 1000 : timeoutRaw, 120000);
|
|
2066
|
+
}
|
|
1828
2067
|
const r = await httpFetch(String(args.url || ''), {
|
|
1829
2068
|
maxChars: Number(args.max_chars) || MAX_FETCH_CHARS,
|
|
2069
|
+
format,
|
|
2070
|
+
timeoutMs,
|
|
1830
2071
|
signal: ctx.signal,
|
|
1831
2072
|
});
|
|
1832
2073
|
return JSON.stringify(r);
|
package/src/main.js
CHANGED
|
@@ -194,6 +194,8 @@ try {
|
|
|
194
194
|
try { nodemailer = require('nodemailer'); } catch {}
|
|
195
195
|
|
|
196
196
|
const APP_DIR = path.join(app.getPath('appData'), 'beast');
|
|
197
|
+
/* mem0-native embedding modeli whisper ile AYNI cache'i kullanır (src/agent/mem0.js okur) */
|
|
198
|
+
process.env.BEAST_MODELS_DIR = path.join(APP_DIR, 'models');
|
|
197
199
|
|
|
198
200
|
/* ÖNCÜ SAĞLAYICILAR (BÖLÜM 9): endpoint bilmeye gerek yok — picker'dan seç,
|
|
199
201
|
sadece API key gir, modeller otomatik çekilir. Hepsi OpenAI-uyumlu. */
|
|
@@ -1671,7 +1673,7 @@ function botToolSet(cfg) {
|
|
|
1671
1673
|
'event_list', 'event_subscribe', 'event_unsubscribe',
|
|
1672
1674
|
'watcher_add', 'watcher_list', 'watcher_remove',
|
|
1673
1675
|
]);
|
|
1674
|
-
if (s.web_search) { set.add('web_search'); set.add('http_fetch'); set.add('deep_search'); }
|
|
1676
|
+
if (s.web_search) { set.add('web_search'); set.add('http_fetch'); set.add('webfetch'); set.add('deep_search'); }
|
|
1675
1677
|
if (s.browser) {
|
|
1676
1678
|
for (const t of ['browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot', 'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select', 'ocr_read']) set.add(t);
|
|
1677
1679
|
}
|
|
@@ -2570,6 +2572,8 @@ function reloadBackend() {
|
|
|
2570
2572
|
search: (id, q, l) => bots.searchMem(id, q, l),
|
|
2571
2573
|
relevant: (id, q) => bots.relevantMem(id, q),
|
|
2572
2574
|
},
|
|
2575
|
+
/* mem0-native hafıza katmanı (semantik arama + konsolidasyon) — ayarlardan kapatılabilir */
|
|
2576
|
+
mem0: settings.mem0 !== false,
|
|
2573
2577
|
roleModels: settings.roleModels || {},
|
|
2574
2578
|
deletedModels: settings.deletedModels || [],
|
|
2575
2579
|
lockdown: !!settings.waLockdown,
|
|
@@ -3975,12 +3979,9 @@ function createWindow() {
|
|
|
3975
3979
|
backgroundColor: dark ? '#0d0d0f' : '#f7f7f8',
|
|
3976
3980
|
icon: path.join(__dirname, '..', 'assets', 'app.ico'),
|
|
3977
3981
|
titleBarStyle: 'hidden',
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
symbolColor: dark ? '#9a9aa2' : '#707078',
|
|
3982
|
-
height: 46,
|
|
3983
|
-
},
|
|
3982
|
+
/* pencere butonları (küçült/büyüt/kapat) renderer'da TRANSPARAN custom
|
|
3983
|
+
çizilir — native titleBarOverlay kendi arka planını dayattığı için
|
|
3984
|
+
kaldırıldı; kontroller 'window:ctrl' IPC'siyle yönetilir */
|
|
3984
3985
|
webPreferences: {
|
|
3985
3986
|
preload: path.join(__dirname, 'preload.js'),
|
|
3986
3987
|
contextIsolation: true,
|
|
@@ -4005,8 +4006,14 @@ function createWindow() {
|
|
|
4005
4006
|
}
|
|
4006
4007
|
});
|
|
4007
4008
|
win.on('resize', layoutBrowser);
|
|
4008
|
-
win.on('maximize',
|
|
4009
|
-
|
|
4009
|
+
win.on('maximize', () => {
|
|
4010
|
+
layoutBrowser();
|
|
4011
|
+
try { if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'win-max', maximized: true }); } catch {}
|
|
4012
|
+
});
|
|
4013
|
+
win.on('unmaximize', () => {
|
|
4014
|
+
layoutBrowser();
|
|
4015
|
+
try { if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'win-max', maximized: false }); } catch {}
|
|
4016
|
+
});
|
|
4010
4017
|
win.on('show', layoutBrowser);
|
|
4011
4018
|
// X'e basınca gizle — tepside yaşamaya devam, WhatsApp bağlantısı sürer
|
|
4012
4019
|
win.on('close', (e) => {
|
|
@@ -5207,18 +5214,25 @@ ipcMain.handle('visible-models:set', (_e, list) => {
|
|
|
5207
5214
|
ipcMain.handle('theme:set', (_e, t) => {
|
|
5208
5215
|
settings.theme = t === 'dark' ? 'dark' : 'light';
|
|
5209
5216
|
saveSettings();
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
win.setTitleBarOverlay(
|
|
5213
|
-
settings.theme === 'dark'
|
|
5214
|
-
? { color: '#0d0d0f', symbolColor: '#9a9aa2' }
|
|
5215
|
-
: { color: '#f7f7f8', symbolColor: '#707078' }
|
|
5216
|
-
);
|
|
5217
|
-
} catch {}
|
|
5218
|
-
}
|
|
5217
|
+
/* native titleBarOverlay kaldırıldı — buton renkleri CSS değişkenleriyle
|
|
5218
|
+
otomatik uyar, main tarafında senkron gerekmez */
|
|
5219
5219
|
return true;
|
|
5220
5220
|
});
|
|
5221
5221
|
|
|
5222
|
+
/* custom pencere butonları (küçült/büyüt/kapat) — renderer'daki .win-controls */
|
|
5223
|
+
ipcMain.handle('window:ctrl', (_e, action) => {
|
|
5224
|
+
if (!win || win.isDestroyed()) return { ok: false };
|
|
5225
|
+
try {
|
|
5226
|
+
if (action === 'minimize') win.minimize();
|
|
5227
|
+
else if (action === 'maximize') win.isMaximized() ? win.unmaximize() : win.maximize();
|
|
5228
|
+
else if (action === 'close') win.close(); /* close handler'ı tray'e gizleme kuralını işletir */
|
|
5229
|
+
else return { ok: false };
|
|
5230
|
+
return { ok: true, maximized: win.isMaximized() };
|
|
5231
|
+
} catch {
|
|
5232
|
+
return { ok: false };
|
|
5233
|
+
}
|
|
5234
|
+
});
|
|
5235
|
+
|
|
5222
5236
|
/* ---------------- cron IPC ---------------- */
|
|
5223
5237
|
|
|
5224
5238
|
function cronEmit() {
|
package/src/preload.js
CHANGED
|
@@ -61,6 +61,7 @@ contextBridge.exposeInMainWorld('beast', {
|
|
|
61
61
|
setVisibleModels: (list) => ipcRenderer.invoke('visible-models:set', list),
|
|
62
62
|
refreshModels: () => ipcRenderer.invoke('models:refresh'),
|
|
63
63
|
setTheme: (t) => ipcRenderer.invoke('theme:set', t),
|
|
64
|
+
winCtrl: (action) => ipcRenderer.invoke('window:ctrl', action),
|
|
64
65
|
toggleBrowser: () => ipcRenderer.invoke('browser:toggle'),
|
|
65
66
|
browserShownGet: () => ipcRenderer.invoke('browser:shown:get'),
|
|
66
67
|
browserShownSet: (v) => ipcRenderer.invoke('browser:shown:set', v),
|
package/src/renderer/i18n.js
CHANGED
|
@@ -126,6 +126,8 @@
|
|
|
126
126
|
tf_saved_toast: 'TinyFish anahtarı kaydedildi',
|
|
127
127
|
tf_cleared_toast: 'TinyFish anahtarı silindi',
|
|
128
128
|
btn_close: 'Kapat',
|
|
129
|
+
tip_win_min: 'Simge durumunda küçült',
|
|
130
|
+
tip_win_max: 'Pencereyi büyüt/küçült',
|
|
129
131
|
em_pass_masked: 'Mevcut şifre korunuyor — değiştirmek için yeniden gir',
|
|
130
132
|
p_h2: 'Provider',
|
|
131
133
|
p_sub: 'Beast config.yaml\u2019dan gelen modeller — aktif olanı seç',
|
|
@@ -688,6 +690,8 @@
|
|
|
688
690
|
tf_saved_toast: 'TinyFish key saved',
|
|
689
691
|
tf_cleared_toast: 'TinyFish key deleted',
|
|
690
692
|
btn_close: 'Close',
|
|
693
|
+
tip_win_min: 'Minimize',
|
|
694
|
+
tip_win_max: 'Maximize/Restore',
|
|
691
695
|
em_pass_masked: 'Current password kept — type a new one to change it',
|
|
692
696
|
p_h2: 'Provider',
|
|
693
697
|
p_sub: 'Models from Beast config.yaml — pick the active one',
|
package/src/renderer/index.html
CHANGED
|
@@ -80,6 +80,11 @@
|
|
|
80
80
|
<button id="browserBtn" class="dd-btn dd-icon-btn" title="Dahili tarayıcı" data-i18n-title="tipBrowser">⧉</button>
|
|
81
81
|
<button id="eyeBtn" class="dd-btn dd-icon-btn" title="Ajan tarayıcısı görünür/gizli — göz açıkken aramalar panelde izlenir, kapalıyken gizli çalışır" data-i18n-title="tipEye"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/><line class="eye-slash" x1="4" y1="4" x2="20" y2="20"/></svg></button>
|
|
82
82
|
<div class="drag-spacer"></div>
|
|
83
|
+
<div class="win-controls" id="winControls">
|
|
84
|
+
<button id="winMin" title="Simge durumunda küçült" data-i18n-title="tip_win_min">─︎</button>
|
|
85
|
+
<button id="winMax" title="Pencereyi büyüt/küçült" data-i18n-title="tip_win_max">□︎</button>
|
|
86
|
+
<button id="winClose" class="win-close" title="Kapat" data-i18n-title="btn_close">✕︎</button>
|
|
87
|
+
</div>
|
|
83
88
|
</header>
|
|
84
89
|
|
|
85
90
|
<div id="chatScroll">
|