beast-agent 2.30.6 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/fsguard.js +46 -0
- package/src/agent/mcp.js +98 -18
- package/src/agent/tools.js +114 -4
- package/src/main.js +106 -21
- package/src/renderer/i18n.js +31 -30
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Dışa gönderim güvenlik kapısı: ajanın send_file aracı sır/anahtar
|
|
4
|
+
dosyalarını asla WhatsApp/sohbete göndermesin. Saf fonksiyon —
|
|
5
|
+
electron'a bağımlı değildir, test edilebilir. */
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
function norm(p) {
|
|
11
|
+
return String(p || '').replace(/\//g, '\\').toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* true → bu dosya hassas, dışa gönderilmez */
|
|
15
|
+
function isSensitiveSendPath(abs, opts = {}) {
|
|
16
|
+
const appData = opts.appData || process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
17
|
+
const home = opts.home || os.homedir();
|
|
18
|
+
let full;
|
|
19
|
+
try {
|
|
20
|
+
full = norm(path.resolve(String(abs || '')));
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (!full) return false;
|
|
25
|
+
|
|
26
|
+
/* Beast veri dizini: settings.json (API key'leri), wa-auth, mcp.json,
|
|
27
|
+
sessions, botlar, tools — tek komutla sızamaz */
|
|
28
|
+
const beastDir = norm(path.join(appData, 'beast'));
|
|
29
|
+
if (full === beastDir || full.startsWith(beastDir + '\\')) return true;
|
|
30
|
+
|
|
31
|
+
/* Kimlik/anahtar klasörleri */
|
|
32
|
+
for (const d of ['.ssh', '.aws', '.gnupg', '.docker', '.kube', '.config']) {
|
|
33
|
+
const p = norm(path.join(home, d));
|
|
34
|
+
if (full === p || full.startsWith(p + '\\')) return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const base = path.basename(full);
|
|
38
|
+
if (/^(id_rsa|id_ed25519|id_ecdsa|id_dsa|authorized_keys)(\.pub)?$/.test(base)) return true;
|
|
39
|
+
if (/^\.env(\.|$)/.test(base)) return true;
|
|
40
|
+
if (/\.(pem|key|pfx|p12|keystore|jks|ppk)$/.test(base)) return true;
|
|
41
|
+
if (/^(credentials|creds|secrets?)\.(json|ya?ml|txt)$/.test(base)) return true;
|
|
42
|
+
if (/^(\.npmrc|\.netrc|\.git-credentials|\.htpasswd)$/.test(base)) return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { isSensitiveSendPath };
|
package/src/agent/mcp.js
CHANGED
|
@@ -42,6 +42,23 @@ function configPath() {
|
|
|
42
42
|
|
|
43
43
|
let cfgCache = { mtime: -1, cfg: { servers: {} } };
|
|
44
44
|
|
|
45
|
+
/* Sunucu kaydını normalize eder + token güvenliğini doğrular.
|
|
46
|
+
Komut yoksa null döner; satır sonu/NUL varsa fırlatır (kayıt atlanır/reddedilir). */
|
|
47
|
+
function normalizeServer(s) {
|
|
48
|
+
if (!s || typeof s !== 'object' || !s.command) return null;
|
|
49
|
+
const command = String(s.command);
|
|
50
|
+
assertSafeToken(command, 'MCP komutu');
|
|
51
|
+
const args = (Array.isArray(s.args) ? s.args : []).map((a) => assertSafeToken(a, 'MCP argümanı'));
|
|
52
|
+
return {
|
|
53
|
+
command,
|
|
54
|
+
args,
|
|
55
|
+
env: s.env && typeof s.env === 'object' ? Object.fromEntries(Object.entries(s.env).map(([k, v]) => [String(k), String(v)])) : {},
|
|
56
|
+
enabled: s.enabled !== false,
|
|
57
|
+
tools: Array.isArray(s.tools) ? s.tools.map(String) : null,
|
|
58
|
+
timeoutMs: Number(s.timeoutMs) > 1000 ? Math.min(Number(s.timeoutMs), 600000) : CALL_TIMEOUT_MS,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
function readConfig(force) {
|
|
46
63
|
try {
|
|
47
64
|
const p = configPath();
|
|
@@ -51,15 +68,12 @@ function readConfig(force) {
|
|
|
51
68
|
const cfg = { servers: {} };
|
|
52
69
|
if (parsed && typeof parsed.servers === 'object') {
|
|
53
70
|
for (const [name, s] of Object.entries(parsed.servers)) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
tools: Array.isArray(s.tools) ? s.tools.map(String) : null,
|
|
61
|
-
timeoutMs: Number(s.timeoutMs) > 1000 ? Math.min(Number(s.timeoutMs), 600000) : CALL_TIMEOUT_MS,
|
|
62
|
-
};
|
|
71
|
+
try {
|
|
72
|
+
const srv = normalizeServer(s);
|
|
73
|
+
if (srv) cfg.servers[sanitizeName(name)] = srv;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
log.warn(`[mcp] '${name}' geçersiz, atlandı: ${String((e && e.message) || e)}`);
|
|
76
|
+
}
|
|
63
77
|
}
|
|
64
78
|
}
|
|
65
79
|
cfgCache = { mtime: st.mtimeMs, cfg };
|
|
@@ -76,9 +90,15 @@ function readConfig(force) {
|
|
|
76
90
|
|
|
77
91
|
function saveConfig(cfg) {
|
|
78
92
|
const p = configPath();
|
|
93
|
+
/* DİSKE YAZMADAN doğrula: güvensiz/yapıbozuk kayıt asla mcp.json'a düşmez */
|
|
94
|
+
const clean = { servers: {} };
|
|
95
|
+
for (const [name, s] of Object.entries((cfg && cfg.servers) || {})) {
|
|
96
|
+
const srv = normalizeServer(s);
|
|
97
|
+
if (srv) clean.servers[sanitizeName(name)] = srv;
|
|
98
|
+
}
|
|
79
99
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
80
|
-
fs.writeFileSync(p, JSON.stringify(
|
|
81
|
-
cfgCache = { mtime: -1, cfg }; /* sonraki okuma diskten gelsin */
|
|
100
|
+
fs.writeFileSync(p, JSON.stringify(clean, null, 2) + '\n');
|
|
101
|
+
cfgCache = { mtime: -1, cfg: clean }; /* sonraki okuma diskten gelsin */
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
function sanitizeName(name) {
|
|
@@ -89,20 +109,77 @@ function sanitizeName(name) {
|
|
|
89
109
|
.slice(0, 32) || 'server';
|
|
90
110
|
}
|
|
91
111
|
|
|
112
|
+
/* UI'a maskeli ('***') giden env sırlarını diskteki mevcut değerlerle geri koyar.
|
|
113
|
+
Böylece kullanıcı formu kaydedince sırlar '***' ile ezilmez. */
|
|
114
|
+
function restoreMaskedSecrets(cfg) {
|
|
115
|
+
let cur;
|
|
116
|
+
try { cur = readConfig(true); } catch { return cfg; }
|
|
117
|
+
for (const [name, s] of Object.entries((cfg && cfg.servers) || {})) {
|
|
118
|
+
if (!s || typeof s !== 'object' || !s.env || typeof s.env !== 'object') continue;
|
|
119
|
+
const old = cur.servers[sanitizeName(name)];
|
|
120
|
+
if (!old || !old.env) continue;
|
|
121
|
+
for (const [k, v] of Object.entries(s.env)) {
|
|
122
|
+
if (String(v) === '***' && old.env[k] !== undefined) s.env[k] = old.env[k];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return cfg;
|
|
126
|
+
}
|
|
127
|
+
|
|
92
128
|
/* ---------- süreç yaşam döngüsü ---------- */
|
|
93
129
|
|
|
130
|
+
/* Token doğrulama: satır sonu/NUL her platformda enjeksiyon vektörüdür */
|
|
131
|
+
function assertSafeToken(t, what) {
|
|
132
|
+
const s = String(t == null ? '' : t);
|
|
133
|
+
if (/[\0\r\n]/.test(s)) throw new Error(what + ' içinde satır sonu/NUL karakteri olamaz');
|
|
134
|
+
return s;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const CMD_META_RE = /[\s"&|<>^()]/;
|
|
138
|
+
|
|
139
|
+
/* KOMUT token'ı: yalnız gerekiyorsa tırnaklanır — aksi halde npm/npx gibi
|
|
140
|
+
.cmd sarmalayıcılarının %~dp0 çözümü bozulur. %/! reddedilir. */
|
|
141
|
+
function quoteCmdCommand(t) {
|
|
142
|
+
const s = assertSafeToken(t, 'MCP komutu');
|
|
143
|
+
if (/[%!]/.test(s)) throw new Error('MCP komutunda % veya ! kullanılamaz (güvenlik)');
|
|
144
|
+
if (!CMD_META_RE.test(s)) return s;
|
|
145
|
+
return '"' + s.replace(/"/g, '""') + '"';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ARGÜMAN token'ı: HER ZAMAN tırnak içinde → & | < > gibi metakarakterler
|
|
149
|
+
komut ayıramaz. %/! genişletilebildiği için reddedilir; kapanış tırnağı
|
|
150
|
+
öncesi ters bölü CRT kuralı gereği çiftlenir. */
|
|
151
|
+
function quoteCmdArg(t) {
|
|
152
|
+
const s = assertSafeToken(t, 'MCP argümanı');
|
|
153
|
+
if (/[%!]/.test(s)) throw new Error('MCP argümanında % veya ! kullanılamaz (güvenlik)');
|
|
154
|
+
const out = s.replace(/"/g, '""').replace(/(\\+)$/, '$1$1');
|
|
155
|
+
return '"' + out + '"';
|
|
156
|
+
}
|
|
157
|
+
|
|
94
158
|
function spawnServer(cfg) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const child = spawn(line, {
|
|
100
|
-
shell: true,
|
|
159
|
+
const cmd = assertSafeToken(cfg.command, 'MCP komutu');
|
|
160
|
+
if (!cmd.trim()) throw new Error('MCP komutu boş');
|
|
161
|
+
const args = (Array.isArray(cfg.args) ? cfg.args : []).map((a) => assertSafeToken(a, 'MCP argümanı'));
|
|
162
|
+
const opts = {
|
|
101
163
|
windowsHide: true,
|
|
102
164
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
103
165
|
env: { ...process.env, ...(cfg.env || {}) },
|
|
166
|
+
};
|
|
167
|
+
if (process.platform !== 'win32') {
|
|
168
|
+
/* POSIX: shell YOK — & | ; $() gibi metakarakterler enjekte edilemez */
|
|
169
|
+
return spawn(cmd, args, { ...opts, shell: false });
|
|
170
|
+
}
|
|
171
|
+
/* Windows: npx/uvx .cmd sarmalayıcıları düz spawn edilemez — cmd.exe'ye
|
|
172
|
+
tırnaklı/tırnaksız DOĞRU komut satırı elle kurulur; shell KAPALI,
|
|
173
|
+
windowsVerbatimArguments ile libuv müdahalesi kapatılır. */
|
|
174
|
+
const first = quoteCmdCommand(cmd);
|
|
175
|
+
const line = [first, ...args.map(quoteCmdArg)].join(' ');
|
|
176
|
+
/* İlk token tırnaklıysa cmd /s ilk ve son tırnağı KIRPAR — dış sarmal şart */
|
|
177
|
+
const full = first.startsWith('"') ? '"' + line + '"' : line;
|
|
178
|
+
return spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', full], {
|
|
179
|
+
...opts,
|
|
180
|
+
shell: false,
|
|
181
|
+
windowsVerbatimArguments: true,
|
|
104
182
|
});
|
|
105
|
-
return child;
|
|
106
183
|
}
|
|
107
184
|
|
|
108
185
|
function connFor(name) {
|
|
@@ -414,6 +491,9 @@ module.exports = {
|
|
|
414
491
|
configPath,
|
|
415
492
|
readConfig,
|
|
416
493
|
saveConfig,
|
|
494
|
+
restoreMaskedSecrets,
|
|
495
|
+
normalizeServer,
|
|
496
|
+
_spawnServer: spawnServer,
|
|
417
497
|
mergeTools,
|
|
418
498
|
toolSchemas,
|
|
419
499
|
call,
|
package/src/agent/tools.js
CHANGED
|
@@ -4,6 +4,8 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const https = require('https');
|
|
7
|
+
const dns = require('dns');
|
|
8
|
+
const net = require('net');
|
|
7
9
|
const { execFile } = require('child_process');
|
|
8
10
|
const { spawn } = require('child_process');
|
|
9
11
|
const research = require('./research');
|
|
@@ -877,6 +879,111 @@ function assertPublicHttpUrl(raw) {
|
|
|
877
879
|
return u.toString();
|
|
878
880
|
}
|
|
879
881
|
|
|
882
|
+
/* Özel/ayrılmış IP aralığı mı? (IPv4 + IPv6 + v4-mapped) */
|
|
883
|
+
function isPrivateIp(ip) {
|
|
884
|
+
const v = String(ip || '').toLowerCase().replace(/^\[|\]$/g, '');
|
|
885
|
+
if (v.includes(':')) {
|
|
886
|
+
if (v === '::1' || v === '::') return true;
|
|
887
|
+
if (/^f[cd]/.test(v)) return true; /* fc00::/7 unique-local */
|
|
888
|
+
if (/^fe[89ab]/.test(v)) return true; /* fe80::/10 link-local */
|
|
889
|
+
if (/^ff/.test(v)) return true; /* multicast */
|
|
890
|
+
if (v.startsWith('2001:db8')) return true; /* dokümantasyon */
|
|
891
|
+
const m = v.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); /* v4-mapped */
|
|
892
|
+
if (m) return isPrivateIp(m[1]);
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
const p = v.split('.').map(Number);
|
|
896
|
+
if (p.length !== 4 || p.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return true; /* çözümlenemeyen → güvensiz */
|
|
897
|
+
if (p[0] === 0 || p[0] === 10 || p[0] === 127 || p[0] >= 224) return true;
|
|
898
|
+
if (p[0] === 100 && p[1] >= 64 && p[1] <= 127) return true; /* CGNAT */
|
|
899
|
+
if (p[0] === 169 && p[1] === 254) return true; /* link-local */
|
|
900
|
+
if (p[0] === 172 && p[1] >= 16 && p[1] <= 31) return true;
|
|
901
|
+
if (p[0] === 192 && (p[1] === 168 || (p[1] === 0 && (p[2] === 0 || p[2] === 2)) || (p[1] === 88 && p[2] === 99))) return true;
|
|
902
|
+
if (p[0] === 198 && (p[1] === 18 || p[1] === 19 || (p[1] === 51 && p[2] === 100))) return true;
|
|
903
|
+
if (p[0] === 203 && p[1] === 0 && p[2] === 113) return true;
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/* DNS çözümlemesi: alan adı ÖZEL bir IP'ye çözülüyorsa (localtest.me vb.) engelle.
|
|
908
|
+
TOCTOU/DNS-rebinding için tam çözüm connection pinning gerektirir; bu katman
|
|
909
|
+
metin kontrolü + gerçek çözümleme + redirect doğrulamasıyla açığı kapatır. */
|
|
910
|
+
async function assertPublicHostResolved(hostname) {
|
|
911
|
+
const h = String(hostname || '').replace(/^\[|\]$/g, '');
|
|
912
|
+
if (!h) throw new Error('geçersiz adres');
|
|
913
|
+
if (net.isIP(h)) {
|
|
914
|
+
if (isPrivateIp(h)) throw new Error('yerel/ağ içi adreslere erişim engellendi');
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
let addrs;
|
|
918
|
+
try {
|
|
919
|
+
addrs = await dns.promises.lookup(h, { all: true, verbatim: true });
|
|
920
|
+
} catch {
|
|
921
|
+
throw new Error('alan adı çözümlenemedi: ' + h);
|
|
922
|
+
}
|
|
923
|
+
if (!addrs || !addrs.length) throw new Error('alan adı çözümlenemedi: ' + h);
|
|
924
|
+
for (const a of addrs) {
|
|
925
|
+
if (isPrivateIp(a.address)) throw new Error('yerel/ağ içi adreslere erişim engellendi (' + h + ')');
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/* http_fetch için: yönlendirmeleri ELLE takip eder ve HER adımı doğrular
|
|
930
|
+
(302 → 127.0.0.1 gibi bypass'ları kapatır). Döner: { res, url } */
|
|
931
|
+
async function fetchPublicFollow(rawUrl, opts = {}, timeoutMs = 30000, signal) {
|
|
932
|
+
let current = assertPublicHttpUrl(rawUrl);
|
|
933
|
+
for (let hop = 0; hop <= 5; hop++) {
|
|
934
|
+
const u = new URL(current);
|
|
935
|
+
await assertPublicHostResolved(u.hostname);
|
|
936
|
+
const ctrl = new AbortController();
|
|
937
|
+
const timer = setTimeout(() => ctrl.abort(new Error('zaman aşımı')), timeoutMs);
|
|
938
|
+
const onAbort = () => ctrl.abort(new Error('iptal'));
|
|
939
|
+
if (signal) {
|
|
940
|
+
if (signal.aborted) throw new Error('iptal');
|
|
941
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
942
|
+
}
|
|
943
|
+
let res;
|
|
944
|
+
try {
|
|
945
|
+
res = await fetch(current, { ...opts, signal: ctrl.signal, redirect: 'manual' });
|
|
946
|
+
} finally {
|
|
947
|
+
clearTimeout(timer);
|
|
948
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
949
|
+
}
|
|
950
|
+
const loc = res.headers.get('location');
|
|
951
|
+
if (res.status >= 300 && res.status < 400 && loc) {
|
|
952
|
+
if (hop === 5) throw new Error('çok fazla yönlendirme');
|
|
953
|
+
current = assertPublicHttpUrl(new URL(loc, current).toString());
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
return { res, url: current };
|
|
957
|
+
}
|
|
958
|
+
throw new Error('çok fazla yönlendirme');
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/* Gövdeyi SINIRLI oku — kötü niyetli sunucu sınırsız veri akıtıp belleği şişirmesin */
|
|
962
|
+
async function readTextCapped(res, byteCap = 1024 * 1024) {
|
|
963
|
+
if (!res.body || typeof res.body.getReader !== 'function') {
|
|
964
|
+
const t = await res.text();
|
|
965
|
+
return t.length > byteCap ? t.slice(0, byteCap) : t;
|
|
966
|
+
}
|
|
967
|
+
const reader = res.body.getReader();
|
|
968
|
+
const chunks = [];
|
|
969
|
+
let size = 0;
|
|
970
|
+
try {
|
|
971
|
+
while (true) {
|
|
972
|
+
const { done, value } = await reader.read();
|
|
973
|
+
if (done) break;
|
|
974
|
+
if (!value || !value.length) continue;
|
|
975
|
+
const room = byteCap - size;
|
|
976
|
+
if (room <= 0) break;
|
|
977
|
+
chunks.push(value.length > room ? value.slice(0, room) : value);
|
|
978
|
+
size += Math.min(value.length, room);
|
|
979
|
+
if (size >= byteCap) break;
|
|
980
|
+
}
|
|
981
|
+
} finally {
|
|
982
|
+
try { await reader.cancel(); } catch {}
|
|
983
|
+
}
|
|
984
|
+
return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8');
|
|
985
|
+
}
|
|
986
|
+
|
|
880
987
|
const ENTITIES = {
|
|
881
988
|
amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', mdash: '—', ndash: '–',
|
|
882
989
|
hellip: '…', rsquo: '\u2019', lsquo: '\u2018', ldquo: '\u201c', rdquo: '\u201d',
|
|
@@ -1004,10 +1111,10 @@ async function webSearch(query, { maxResults = 8, signal } = {}) {
|
|
|
1004
1111
|
const MAX_FETCH_CHARS = 9000;
|
|
1005
1112
|
|
|
1006
1113
|
async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, format = 'text', timeoutMs = 30000, signal } = {}) {
|
|
1007
|
-
const safe = assertPublicHttpUrl(url);
|
|
1008
1114
|
const t = Math.min(Math.max(Number(timeoutMs) || 30000, 1000), 120000); // opencode: default 30s, max 120s
|
|
1009
|
-
|
|
1010
|
-
|
|
1115
|
+
/* SSRF: metin + DNS çözümlemesi + her redirect adımı ayrı doğrulanır */
|
|
1116
|
+
const { res, url: safe } = await fetchPublicFollow(
|
|
1117
|
+
url,
|
|
1011
1118
|
{ headers: { 'User-Agent': UA, Accept: 'text/html,text/plain,application/json;q=0.9,*/*;q=0.5' } },
|
|
1012
1119
|
t,
|
|
1013
1120
|
signal
|
|
@@ -1020,7 +1127,7 @@ async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, format = 'text', tim
|
|
|
1020
1127
|
return { ok: true, url: safe, status: res.status, contentType: ctype, note: 'ikili/metin olmayan içerik indirilmedi' };
|
|
1021
1128
|
}
|
|
1022
1129
|
const cap = 400000;
|
|
1023
|
-
let body = await res
|
|
1130
|
+
let body = await readTextCapped(res, 1024 * 1024);
|
|
1024
1131
|
if (body.length > cap) body = body.slice(0, cap);
|
|
1025
1132
|
let content;
|
|
1026
1133
|
if (/html/i.test(ctype)) {
|
|
@@ -2357,6 +2464,9 @@ module.exports = {
|
|
|
2357
2464
|
globToRegExp,
|
|
2358
2465
|
walkFiles,
|
|
2359
2466
|
assertPublicHttpUrl,
|
|
2467
|
+
isPrivateIp,
|
|
2468
|
+
assertPublicHostResolved,
|
|
2469
|
+
fetchPublicFollow,
|
|
2360
2470
|
parseDdgResults,
|
|
2361
2471
|
htmlToText,
|
|
2362
2472
|
webSearch,
|
package/src/main.js
CHANGED
|
@@ -26,10 +26,15 @@ const watchers = require('./agent/watchers');
|
|
|
26
26
|
const usageMod = require('./agent/usage');
|
|
27
27
|
const bus = require('./agent/bus');
|
|
28
28
|
const computeruse = require('./agent/computeruse');
|
|
29
|
+
const fsguard = require('./agent/fsguard');
|
|
29
30
|
const log = require('./agent/logger');
|
|
30
31
|
const headroom = require('./agent/headroom');
|
|
31
32
|
const QRCode = require('qrcode'); /* Expo Go QR (bc-expurl) — whatsapp ile aynı paket */
|
|
32
33
|
|
|
34
|
+
/* Renderer'a sır gönderirken kullanılan maske; kaydederken aynen geri gelirse
|
|
35
|
+
gerçek değer korunur (anahtarlar UI'da düz metin dolaşmaz). */
|
|
36
|
+
const SECRET_MASK = '***';
|
|
37
|
+
|
|
33
38
|
/* #3 otomatik updater: sessiz — indirir, kapanışta kurar, kullanıcıya soru sormaz.
|
|
34
39
|
Paketlenmemiş (npm start) modda devre dışı; Update sekmesi ve /update komutu kontrol eder. */
|
|
35
40
|
let autoUpdater = null;
|
|
@@ -2337,20 +2342,34 @@ async function handleWaIncoming(jid, payload, senderNum) {
|
|
|
2337
2342
|
if (!engine) return;
|
|
2338
2343
|
if (typeof payload === 'string') payload = { text: payload };
|
|
2339
2344
|
const isGroup = !!payload.isGroup || jid.endsWith('@g.us');
|
|
2345
|
+
/* İzin listesi kaydı — grup kapıları ve doğal dil komutları bunu kullanır:
|
|
2346
|
+
izinli değilse yalnız sohbet; agentic iş yaptıramaz. */
|
|
2347
|
+
const senderHit = waFind(senderNum);
|
|
2340
2348
|
|
|
2341
|
-
/* slash komutları: DM'de her zaman; grupta sadece bot mention edildiyse
|
|
2349
|
+
/* slash komutları: DM'de her zaman; grupta sadece bot mention edildiyse.
|
|
2350
|
+
GRUP KAPISI: izin listesinde olmayan üye slash (agentic) komut kullanamaz —
|
|
2351
|
+
herkes sohbet eder ama işi yalnız izinli kişiler yaptırır. */
|
|
2342
2352
|
const txt0 = String(payload.text || '').trim();
|
|
2343
2353
|
if (txt0.startsWith('/') && !txt0.includes('\n') && (!isGroup || payload.mentioned)) {
|
|
2354
|
+
if (isGroup && !senderHit && !/^\/(help|version)\b/i.test(txt0)) {
|
|
2355
|
+
await sendWaSafe(
|
|
2356
|
+
jid,
|
|
2357
|
+
'Bu komutu yalnız izinli kişiler kullanabilir. Sohbet için beni @mention ile çağırabilirsin.'
|
|
2358
|
+
).catch(() => {});
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2344
2361
|
if (await tryWaSlash(jid, txt0, senderNum, payload)) return;
|
|
2345
2362
|
}
|
|
2346
2363
|
|
|
2347
2364
|
/* "hepsini sil" / "tüm cron" / "cronları temizle" gibi doğal dil → cron temizle.
|
|
2365
|
+
Yalnız İZİNLİ gönderen: misafir grup üyesi cron silemez.
|
|
2348
2366
|
clearAll() bellekteki job'ları VE diski boşaltır, zamanlayıcıyı durdurur
|
|
2349
2367
|
(sadece dosya silmek yetmezdi: çalışan proses belleği geri yazıyordu). */
|
|
2350
2368
|
const lc = txt0.toLowerCase();
|
|
2351
2369
|
if (
|
|
2352
|
-
|
|
2353
|
-
|
|
2370
|
+
senderHit &&
|
|
2371
|
+
((lc.includes('cron') && /(sil|temizle|kaldır|hepsi|tüm|sıfırla)/.test(lc)) ||
|
|
2372
|
+
/^(hepsini|hepsını|tümünü|tümünü|hepsnı|hepsnı)\b.*(sil|sıl|temizle|kaldır)/.test(lc))
|
|
2354
2373
|
) {
|
|
2355
2374
|
const r = cron.clearAll();
|
|
2356
2375
|
cronEmit();
|
|
@@ -2364,8 +2383,10 @@ async function handleWaIncoming(jid, payload, senderNum) {
|
|
|
2364
2383
|
}
|
|
2365
2384
|
|
|
2366
2385
|
/* "hatırlatmaları sil" / "alarm temizle" / "reminders kaldır" → yalnızca
|
|
2367
|
-
otomatik hatırlatmalar silinir (cron görevlerine dokunulmaz).
|
|
2386
|
+
otomatik hatırlatmalar silinir (cron görevlerine dokunulmaz).
|
|
2387
|
+
Yalnız İZİNLİ gönderen tetikleyebilir. */
|
|
2368
2388
|
if (
|
|
2389
|
+
senderHit &&
|
|
2369
2390
|
/hat[iı]rlat|alarm|remind/.test(lc) &&
|
|
2370
2391
|
/(sil|sıl|temizle|kaldır|iptal|sıfırla|kapat)/.test(lc)
|
|
2371
2392
|
) {
|
|
@@ -2505,7 +2526,10 @@ async function mqueueTick() {
|
|
|
2505
2526
|
|
|
2506
2527
|
async function processWaMessage(jid, payload, senderNum, requeues = 0) {
|
|
2507
2528
|
const isGroup = !!payload.isGroup;
|
|
2508
|
-
|
|
2529
|
+
/* GRUP: gönderen izin listesinde mi? İzinli → kendi yetki seviyesi;
|
|
2530
|
+
değilse 'chat' (sohbet serbest, araç YOK — agentic işi yalnız izinliler
|
|
2531
|
+
yaptırabilir). DM'de izinsiz gönderen buraya gelemez. */
|
|
2532
|
+
const hit = waFind(senderNum);
|
|
2509
2533
|
if (!isGroup && !hit) {
|
|
2510
2534
|
waLog(`skip flush: izinli eşleşme yok (sender=+${senderNum || '?'})`);
|
|
2511
2535
|
return;
|
|
@@ -2522,8 +2546,8 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
|
|
|
2522
2546
|
waLastActiveJid = String(jid); // cevap/dosya hedefi: en son yazan sohbet
|
|
2523
2547
|
// Cevap verilecek — karşı telefonda "yazıyor…" göstergesi (medya işlenene dek sürer)
|
|
2524
2548
|
wa.setComposing(jid, true);
|
|
2525
|
-
// Kişi bazlı granül izin: all/web/read/chat
|
|
2526
|
-
let perm =
|
|
2549
|
+
// Kişi bazlı granül izin: all/web/read/chat — grup misafiri varsayılan 'chat'
|
|
2550
|
+
let perm = hit ? hit.perm || (hit.lockdown ? 'chat' : 'all') : 'chat';
|
|
2527
2551
|
engine.setSessionPerm(sid, perm);
|
|
2528
2552
|
|
|
2529
2553
|
/* BOT SİSTEMİ: numara → bot eşleştirme (izinli kayıtta bot_id yoksa beast'e düşer) */
|
|
@@ -6111,13 +6135,36 @@ ipcMain.handle('wa:groups:set', (_e, cfg) => {
|
|
|
6111
6135
|
ipcMain.handle('settings:get', () => {
|
|
6112
6136
|
const out = JSON.parse(JSON.stringify(settings));
|
|
6113
6137
|
/* Sırların renderera düz metin gitmesini engelle */
|
|
6114
|
-
if (out.email && out.email.pass) out.email.pass =
|
|
6115
|
-
if (out.waTts && out.waTts.key) out.waTts.key =
|
|
6138
|
+
if (out.email && out.email.pass) out.email.pass = SECRET_MASK;
|
|
6139
|
+
if (out.waTts && out.waTts.key) out.waTts.key = SECRET_MASK;
|
|
6140
|
+
if (out.supermemory && out.supermemory.apiKey) out.supermemory.apiKey = SECRET_MASK;
|
|
6141
|
+
if (out.fallout && Array.isArray(out.fallout.slots)) {
|
|
6142
|
+
for (const s of out.fallout.slots) if (s && s.key) s.key = SECRET_MASK;
|
|
6143
|
+
}
|
|
6144
|
+
if (Array.isArray(out.customProviders)) {
|
|
6145
|
+
for (const p of out.customProviders) if (p && p.key) p.key = SECRET_MASK;
|
|
6146
|
+
}
|
|
6116
6147
|
return out;
|
|
6117
6148
|
});
|
|
6118
6149
|
|
|
6119
6150
|
/* ---------------- MCP IPC ---------------- */
|
|
6120
6151
|
|
|
6152
|
+
/* UI'a giden raw JSON'da env sırlarını maskeler (düz metin anahtar sızmasın) */
|
|
6153
|
+
function maskMcpRaw(raw) {
|
|
6154
|
+
try {
|
|
6155
|
+
const obj = JSON.parse(String(raw || ''));
|
|
6156
|
+
if (!obj || typeof obj !== 'object' || !obj.servers || typeof obj.servers !== 'object') return raw;
|
|
6157
|
+
for (const s of Object.values(obj.servers)) {
|
|
6158
|
+
if (s && typeof s === 'object' && s.env && typeof s.env === 'object') {
|
|
6159
|
+
for (const k of Object.keys(s.env)) if (String(s.env[k] || '')) s.env[k] = SECRET_MASK;
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
return JSON.stringify(obj, null, 2);
|
|
6163
|
+
} catch {
|
|
6164
|
+
return raw;
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
|
|
6121
6168
|
ipcMain.handle('mcp:status', () => {
|
|
6122
6169
|
try { return mcpMod.status(); } catch { return { path: '', servers: [] }; }
|
|
6123
6170
|
});
|
|
@@ -6127,15 +6174,17 @@ ipcMain.handle('mcp:config:get', () => {
|
|
|
6127
6174
|
const p = mcpMod.configPath();
|
|
6128
6175
|
let raw = '';
|
|
6129
6176
|
try { raw = require('fs').readFileSync(p, 'utf8'); } catch {}
|
|
6130
|
-
return { path: p, raw };
|
|
6177
|
+
return { path: p, raw: maskMcpRaw(raw) };
|
|
6131
6178
|
} catch { return { path: '', raw: '' }; }
|
|
6132
6179
|
});
|
|
6133
6180
|
|
|
6134
6181
|
ipcMain.handle('mcp:config:set', (_e, raw) => {
|
|
6135
6182
|
try {
|
|
6136
6183
|
const text = String(raw || '');
|
|
6137
|
-
|
|
6138
|
-
|
|
6184
|
+
const cfg = text.trim() ? JSON.parse(text) : { servers: {} };
|
|
6185
|
+
/* maskeli env sırlarını mevcut diskteki değerlerle geri koy */
|
|
6186
|
+
try { mcpMod.restoreMaskedSecrets(cfg); } catch {}
|
|
6187
|
+
mcpMod.saveConfig(cfg);
|
|
6139
6188
|
return { ok: true };
|
|
6140
6189
|
} catch (e) {
|
|
6141
6190
|
return { ok: false, error: String((e && e.message) || e) };
|
|
@@ -6163,31 +6212,45 @@ ipcMain.handle('fallout:get', () => {
|
|
|
6163
6212
|
out.enabled = !!f.enabled;
|
|
6164
6213
|
out.autoResume = f.autoResume !== false;
|
|
6165
6214
|
if (Array.isArray(f.slots)) {
|
|
6166
|
-
for (let i = 0; i < Math.min(10, f.slots.length); i++)
|
|
6215
|
+
for (let i = 0; i < Math.min(10, f.slots.length); i++) {
|
|
6216
|
+
const s = f.slots[i];
|
|
6217
|
+
/* kayıtlı anahtarlar UI'a maskeli gider */
|
|
6218
|
+
out.slots[i] = s && s.key ? { ...s, key: SECRET_MASK } : s || null;
|
|
6219
|
+
}
|
|
6167
6220
|
}
|
|
6168
6221
|
return out;
|
|
6169
6222
|
});
|
|
6170
6223
|
|
|
6171
6224
|
ipcMain.handle('fallout:set', (_e, cfg) => {
|
|
6172
6225
|
const cur = settings.fallout || defaultFallout();
|
|
6226
|
+
const prevSlots = Array.isArray(cur.slots) ? cur.slots : [];
|
|
6173
6227
|
const next = defaultFallout();
|
|
6174
6228
|
next.enabled = !!(cfg && cfg.enabled);
|
|
6175
6229
|
next.autoResume = !cfg || cfg.autoResume !== false;
|
|
6176
6230
|
const slots = Array.isArray(cfg && cfg.slots) ? cfg.slots : [];
|
|
6177
6231
|
for (let i = 0; i < Math.min(10, slots.length); i++) {
|
|
6178
6232
|
const s = slots[i];
|
|
6179
|
-
if (!s || typeof s !== 'object' || !s.providerId || !s.model
|
|
6233
|
+
if (!s || typeof s !== 'object' || !s.providerId || !s.model) continue;
|
|
6234
|
+
let key = String(s.key || '');
|
|
6235
|
+
/* maskeli/boş anahtar: aynı provider'ın önceki kayıtlı anahtarı korunur */
|
|
6236
|
+
if (key === SECRET_MASK || !key) {
|
|
6237
|
+
const prev = prevSlots[i];
|
|
6238
|
+
key = prev && prev.providerId === String(s.providerId) && prev.key ? String(prev.key) : '';
|
|
6239
|
+
}
|
|
6240
|
+
if (!key) continue;
|
|
6180
6241
|
next.slots[i] = {
|
|
6181
6242
|
providerId: String(s.providerId),
|
|
6182
6243
|
providerName: String(s.providerName || s.providerId),
|
|
6183
6244
|
model: String(s.model),
|
|
6184
|
-
key
|
|
6245
|
+
key,
|
|
6185
6246
|
};
|
|
6186
6247
|
}
|
|
6187
6248
|
settings.fallout = next;
|
|
6188
6249
|
saveSettings();
|
|
6189
6250
|
if (engine) engine.setFallout(next);
|
|
6190
|
-
|
|
6251
|
+
const out = JSON.parse(JSON.stringify(next));
|
|
6252
|
+
for (const s of out.slots) if (s && s.key) s.key = SECRET_MASK;
|
|
6253
|
+
return out;
|
|
6191
6254
|
});
|
|
6192
6255
|
|
|
6193
6256
|
/* #Limit: provider bazlı max input token limiti + bağlam sıkıştırma */
|
|
@@ -6217,10 +6280,13 @@ ipcMain.handle('sec:set', (_e, cfg) => {
|
|
|
6217
6280
|
ipcMain.handle('supermemory:set', (_e, cfg) => {
|
|
6218
6281
|
try {
|
|
6219
6282
|
const base = String((cfg && cfg.baseUrl) || '').trim() || 'http://localhost:6767';
|
|
6283
|
+
const incomingKey = String((cfg && cfg.apiKey) || '').trim();
|
|
6284
|
+
const prevKey = settings.supermemory && settings.supermemory.apiKey ? String(settings.supermemory.apiKey) : '';
|
|
6220
6285
|
settings.supermemory = {
|
|
6221
6286
|
enabled: !!(cfg && cfg.enabled),
|
|
6222
6287
|
baseUrl: base.replace(/\/+$/, ''),
|
|
6223
|
-
|
|
6288
|
+
/* maskeli anahtar geri gelirse mevcut korunur */
|
|
6289
|
+
apiKey: incomingKey === SECRET_MASK ? prevKey : incomingKey,
|
|
6224
6290
|
containerTag: String((cfg && cfg.containerTag) || 'beast').trim() || 'beast',
|
|
6225
6291
|
};
|
|
6226
6292
|
saveSettings();
|
|
@@ -6230,7 +6296,7 @@ ipcMain.handle('supermemory:set', (_e, cfg) => {
|
|
|
6230
6296
|
engine._smCheckedAt = 0;
|
|
6231
6297
|
engine._smWarned = false;
|
|
6232
6298
|
}
|
|
6233
|
-
return { ok: true, supermemory: settings.supermemory };
|
|
6299
|
+
return { ok: true, supermemory: { ...settings.supermemory, apiKey: settings.supermemory.apiKey ? SECRET_MASK : '' } };
|
|
6234
6300
|
} catch (e) {
|
|
6235
6301
|
return { ok: false, error: String((e && e.message) || e) };
|
|
6236
6302
|
}
|
|
@@ -6240,7 +6306,7 @@ ipcMain.handle('supermemory:get', () => {
|
|
|
6240
6306
|
return {
|
|
6241
6307
|
enabled: sm.enabled !== false,
|
|
6242
6308
|
baseUrl: sm.baseUrl || 'http://localhost:6767',
|
|
6243
|
-
apiKey: sm.apiKey
|
|
6309
|
+
apiKey: sm.apiKey ? SECRET_MASK : '',
|
|
6244
6310
|
containerTag: sm.containerTag || 'beast',
|
|
6245
6311
|
up: engine ? !!engine._smUp : null,
|
|
6246
6312
|
};
|
|
@@ -7317,7 +7383,10 @@ ipcMain.handle('providers:keys', () => {
|
|
|
7317
7383
|
}
|
|
7318
7384
|
} catch {}
|
|
7319
7385
|
}
|
|
7320
|
-
|
|
7386
|
+
/* UI'a anahtarlar DÜZ METİN gitmez — yalnız "kayıtlı var mı" bilgisi taşınır */
|
|
7387
|
+
const out = {};
|
|
7388
|
+
for (const [k, v] of Object.entries(map)) out[k] = String(v || '') ? SECRET_MASK : '';
|
|
7389
|
+
return out;
|
|
7321
7390
|
});
|
|
7322
7391
|
/* #22 izleyici paneli IPC */
|
|
7323
7392
|
ipcMain.handle('watchers:list', () => watchers.list());
|
|
@@ -10266,7 +10335,17 @@ ipcMain.handle('ide:previewUrl', async (_e, url) => {
|
|
|
10266
10335
|
});
|
|
10267
10336
|
|
|
10268
10337
|
ipcMain.handle('custom:set', (_e, list) => {
|
|
10269
|
-
|
|
10338
|
+
const prev = Array.isArray(settings.customProviders) ? settings.customProviders : [];
|
|
10339
|
+
const byId = new Map(prev.filter((p) => p && p.id).map((p) => [p.id, p]));
|
|
10340
|
+
settings.customProviders = (Array.isArray(list) ? list : []).map((p) => {
|
|
10341
|
+
if (!p || typeof p !== 'object') return p;
|
|
10342
|
+
const item = { ...p };
|
|
10343
|
+
const old = byId.get(item.id);
|
|
10344
|
+
const k = String(item.key == null ? '' : item.key).trim();
|
|
10345
|
+
/* UI'dan maskeli/boş anahtar geldiyse aynı id'nin kayıtlı anahtarı korunur */
|
|
10346
|
+
if ((k === SECRET_MASK || !k) && old && old.key) item.key = String(old.key);
|
|
10347
|
+
return item;
|
|
10348
|
+
});
|
|
10270
10349
|
saveSettings();
|
|
10271
10350
|
engine.setCustomProviders(settings.customProviders);
|
|
10272
10351
|
headroom.syncProviders(); /* headroom açıksa proxy setini yeni listeye uydur */
|
|
@@ -10895,6 +10974,12 @@ async function deliverFile(sessionId, filePath, caption) {
|
|
|
10895
10974
|
try {
|
|
10896
10975
|
const abs = path.isAbsolute(filePath) ? filePath : path.join(engine.workspace || process.cwd(), filePath);
|
|
10897
10976
|
if (!fs.existsSync(abs)) return { ok: false, error: 'dosya bulunamadı: ' + abs };
|
|
10977
|
+
/* GÜVENLİK: sır/anahtar dosyaları dışa gönderilmez (settings.json,
|
|
10978
|
+
wa-auth, .ssh, .env, *.pem...). Ajan bunu kullanıcıya söylesin. */
|
|
10979
|
+
if (fsguard.isSensitiveSendPath(abs)) {
|
|
10980
|
+
log.warn('sec', `send_file engellendi (hassas yol): ${abs}`);
|
|
10981
|
+
return { ok: false, error: 'güvenlik: hassas dosya (sır/anahtar) dışa gönderilmedi: ' + path.basename(abs) };
|
|
10982
|
+
}
|
|
10898
10983
|
const name = path.basename(abs);
|
|
10899
10984
|
const ext = path.extname(abs).toLowerCase();
|
|
10900
10985
|
let sid = String(sessionId || '');
|
package/src/renderer/i18n.js
CHANGED
|
@@ -656,6 +656,37 @@
|
|
|
656
656
|
store_install_fail: 'Kurulum başarısız',
|
|
657
657
|
store_like_ok: 'Beğeni kaydedildi',
|
|
658
658
|
store_login_note: 'Beast Kimliği anonimdir; gerçek Beast Kodun asla paylaşılmaz.',
|
|
659
|
+
/* Cron paneli + WhatsApp kişi/yetki editörü */
|
|
660
|
+
cr_empty: 'Henüz görev yok — yukarıdan ekle.',
|
|
661
|
+
cr_dot_on: 'aktif — tıkla: forma doldur',
|
|
662
|
+
cr_dot_off: 'kapalı — tıkla: forma doldur',
|
|
663
|
+
cr_next: 'sıradaki: ',
|
|
664
|
+
cr_last: ' · son: ',
|
|
665
|
+
cr_run: 'Şimdi çalıştır',
|
|
666
|
+
cr_pause: 'Durdur',
|
|
667
|
+
cr_start: 'Başlat',
|
|
668
|
+
cr_del: 'Sil',
|
|
669
|
+
cr_modal_empty: 'Kayıtlı cron görev yok — Ayarlar → Cron sekmesinden ekle.',
|
|
670
|
+
cr_ran: 'Çalıştırıldı',
|
|
671
|
+
cr_toggle_pause: 'Duraklat',
|
|
672
|
+
wa_vazgec: 'Vazgeç',
|
|
673
|
+
wa_kaydet: 'kaydet',
|
|
674
|
+
wa_updated: 'Güncellendi',
|
|
675
|
+
wa_isim_ph: 'isim',
|
|
676
|
+
wa_numara_ph: 'numara',
|
|
677
|
+
wa_no_name_title: 'Bu kişiye cevap VERİLMEYECEK — tıklayıp isim ver',
|
|
678
|
+
wa_edit_title: 'Tıkla: isim/numara düzenle',
|
|
679
|
+
wa_perm_title: 'Yetki seviyesi — bu kişinin agent erişimini belirler',
|
|
680
|
+
wa_owner_title_on: 'Bu kişi BEAST\u2019İN SAHİBİ — tıklayıp başka kişiye devret',
|
|
681
|
+
wa_owner_title_off: 'Tıkla: bu kişiyi SAHİP yap (yalnız biri olabilir)',
|
|
682
|
+
wa_owner_on: '👑 sahip',
|
|
683
|
+
wa_owner_off: 'sahip yap',
|
|
684
|
+
wa_perm_all: 'serbest',
|
|
685
|
+
wa_perm_read: 'okuma',
|
|
686
|
+
wa_perm_chat: 'kısıtlı',
|
|
687
|
+
wa_queue_info: 'Mesaj kuyruğu: ${n} bekleyen',
|
|
688
|
+
wa_queue_failed: '${n} gönderilemedi',
|
|
689
|
+
wa_queue_empty_ok: 'Kuyruk temiz',
|
|
659
690
|
},
|
|
660
691
|
en: {
|
|
661
692
|
newChat: '+ New Chat',
|
|
@@ -1076,36 +1107,6 @@
|
|
|
1076
1107
|
wa_queue_info: 'Message queue: ${n} pending',
|
|
1077
1108
|
wa_queue_failed: '${n} could not be sent',
|
|
1078
1109
|
wa_queue_empty_ok: 'Queue clear',
|
|
1079
|
-
cr_empty: 'Henüz görev yok — yukarıdan ekle.',
|
|
1080
|
-
cr_dot_on: 'aktif — tıkla: forma doldur',
|
|
1081
|
-
cr_dot_off: 'kapalı — tıkla: forma doldur',
|
|
1082
|
-
cr_next: 'sıradaki: ',
|
|
1083
|
-
cr_last: ' · son: ',
|
|
1084
|
-
cr_run: 'Şimdi çalıştır',
|
|
1085
|
-
cr_pause: 'Durdur',
|
|
1086
|
-
cr_start: 'Başlat',
|
|
1087
|
-
cr_del: 'Sil',
|
|
1088
|
-
cr_modal_empty: 'Kayıtlı cron görev yok — Ayarlar → Cron sekmesinden ekle.',
|
|
1089
|
-
cr_ran: 'Çalıştırıldı',
|
|
1090
|
-
cr_toggle_pause: 'Duraklat',
|
|
1091
|
-
wa_vazgec: 'Vazgeç',
|
|
1092
|
-
wa_kaydet: 'kaydet',
|
|
1093
|
-
wa_updated: 'Güncellendi',
|
|
1094
|
-
wa_isim_ph: 'isim',
|
|
1095
|
-
wa_numara_ph: 'numara',
|
|
1096
|
-
wa_no_name_title: 'Bu kişiye cevap VERİLMEYECEK — tıklayıp isim ver',
|
|
1097
|
-
wa_edit_title: 'Tıkla: isim/numara düzenle',
|
|
1098
|
-
wa_perm_title: 'Yetki seviyesi — bu kişinin agent erişimini belirler',
|
|
1099
|
-
wa_owner_title_on: 'Bu kişi BEAST\u2019İN SAHİBİ — tıklayıp başka kişiye devret',
|
|
1100
|
-
wa_owner_title_off: 'Tıkla: bu kişiyi SAHİP yap (yalnız biri olabilir)',
|
|
1101
|
-
wa_owner_on: '👑 sahip',
|
|
1102
|
-
wa_owner_off: 'sahip yap',
|
|
1103
|
-
wa_perm_all: 'serbest',
|
|
1104
|
-
wa_perm_read: 'okuma',
|
|
1105
|
-
wa_perm_chat: 'kısıtlı',
|
|
1106
|
-
wa_queue_info: 'Mesaj kuyruğu: ${n} bekleyen',
|
|
1107
|
-
wa_queue_failed: '${n} gönderilemedi',
|
|
1108
|
-
wa_queue_empty_ok: 'Kuyruk temiz',
|
|
1109
1110
|
us_reset_toast: 'Usage counters reset',
|
|
1110
1111
|
us_backup_ok: 'Backup taken: Desktop\\Beast-Backups',
|
|
1111
1112
|
us_where_on_toast: 'Startup summary on',
|