beast-agent 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +130 -128
  3. package/bin/beast-agent.js +137 -137
  4. package/package.json +1 -1
  5. package/scripts/fix-electron.js +138 -138
  6. package/scripts/release.js +137 -131
  7. package/scripts/swap-electron.js +40 -40
  8. package/src/agent/agentdefs.js +122 -122
  9. package/src/agent/bots.js +580 -580
  10. package/src/agent/bus.js +389 -389
  11. package/src/agent/computeruse.js +200 -200
  12. package/src/agent/config.js +284 -284
  13. package/src/agent/discord.js +332 -332
  14. package/src/agent/engine.js +85 -11
  15. package/src/agent/kb.js +123 -123
  16. package/src/agent/llm.js +430 -430
  17. package/src/agent/logger.js +90 -90
  18. package/src/agent/mcp.js +427 -0
  19. package/src/agent/mem0.js +605 -605
  20. package/src/agent/memory.js +427 -427
  21. package/src/agent/mqueue.js +124 -124
  22. package/src/agent/pdf.js +20 -20
  23. package/src/agent/research.js +133 -133
  24. package/src/agent/scripts/news.py +113 -113
  25. package/src/agent/scripts/stealthsearch.py +30 -30
  26. package/src/agent/scripts/websearch.py +225 -225
  27. package/src/agent/searxng.js +325 -325
  28. package/src/agent/seeds/brainstorming/SKILL.md +90 -90
  29. package/src/agent/seeds/dispatching-parallel-agents/SKILL.md +120 -120
  30. package/src/agent/seeds/executing-plans/SKILL.md +60 -60
  31. package/src/agent/seeds/subagent-driven-development/SKILL.md +167 -167
  32. package/src/agent/seeds/systematic-debugging/SKILL.md +131 -131
  33. package/src/agent/seeds/test-driven-development/SKILL.md +152 -152
  34. package/src/agent/seeds/verification-before-completion/SKILL.md +63 -63
  35. package/src/agent/seeds/writing-plans/SKILL.md +162 -162
  36. package/src/agent/seeds/writing-skills/SKILL.md +229 -229
  37. package/src/agent/skills.js +652 -651
  38. package/src/agent/store.js +378 -378
  39. package/src/agent/telegram.js +155 -155
  40. package/src/agent/tokens.js +39 -39
  41. package/src/agent/usage.js +125 -125
  42. package/src/agent/watchers.js +312 -312
  43. package/src/agent/watext.js +80 -80
  44. package/src/agent/whatsapp.js +555 -555
  45. package/src/cron.js +255 -255
  46. package/src/main.js +385 -6
  47. package/src/preload.js +13 -0
  48. package/src/renderer/browserPreload.js +73 -73
  49. package/src/renderer/i18n.js +38 -0
  50. package/src/renderer/index.html +448 -407
  51. package/src/renderer/renderer.js +897 -42
  52. package/src/renderer/style.css +275 -11
@@ -1,427 +1,427 @@
1
- 'use strict';
2
-
3
- /* Beast memory: SOUL.md + MEMORY.md + USER.md under %APPDATA%\beast\memories.
4
- Tiny and append-only; the model writes via the memory_write tool.
5
- MEMORY.md is NOT dumped into the prompt anymore: buildSystem pulls only
6
- entries relevant to the current query (retrieval) and the agent can call
7
- memory_search to dig deeper on demand. */
8
-
9
- const fs = require('fs');
10
- const path = require('path');
11
- const os = require('os');
12
-
13
- function beastRoot() {
14
- if (process.env.BEAST_DATA) return process.env.BEAST_DATA; // test/taşınabilirlik
15
- return process.env.APPDATA
16
- ? path.join(process.env.APPDATA, 'beast')
17
- : path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
18
- }
19
-
20
- function memDir() {
21
- return path.join(beastRoot(), 'memories');
22
- }
23
-
24
- function ensure() {
25
- fs.mkdirSync(memDir(), { recursive: true });
26
- const soulPath = path.join(memDir(), 'SOUL.md');
27
- if (!fs.existsSync(soulPath)) {
28
- /* SOUL.md = ajanın kişiliği. Kullanıcı Ayarlar → Memory'den düzenler;
29
- bu dosyanın tamamı her istekte sistem promptuna birebir girer. */
30
- const seed = [
31
- 'Sen Beast Agent\u2019sın — kullanıcının Windows makinesinde yerel çalışan hızlı, hafif ve becerikli bir asistan.',
32
- '',
33
- 'KİŞİLİK',
34
- '- Doğrudan ve netsin; lafı dolandırmazsın, gereksiz özür ve dolgu cümle kurmazsın.',
35
- '- Samimi ama işini ciddiye alırsın; kullanıcıya güven verirsin.',
36
- '- Proaktiftir: yapabildiğini yapar, mümkünse sormak yerine yapar.',
37
- '',
38
- 'TON VE DAVRANIŞ',
39
- '- Kısa ve öz cevap ver; madde işaretlerinde "-" kullan.',
40
- '- Yaptığın işleri kısaca raporlarsın; hata olursa nasıl düzeltileceğini da söylersin.',
41
- '',
42
- '(Kişiliğini değiştirmek için bu dosyayı düzenle — yazdığın her şey ajanı tanımlar.)',
43
- ].join('\n');
44
- fs.writeFileSync(soulPath, seed);
45
- }
46
- for (const f of ['MEMORY.md', 'USER.md']) {
47
- const p = path.join(memDir(), f);
48
- if (!fs.existsSync(p)) fs.writeFileSync(p, '');
49
- }
50
- }
51
-
52
- function read(f) {
53
- try {
54
- return fs.readFileSync(path.join(memDir(), f), 'utf8').trim();
55
- } catch {
56
- return '';
57
- }
58
- }
59
-
60
- function loadAll() {
61
- ensure();
62
- return { soul: read('SOUL.md'), user: read('USER.md'), memory: read('MEMORY.md') };
63
- }
64
-
65
- function append(text) {
66
- try {
67
- ensure();
68
- const t = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 500);
69
- if (!t) return { ok: false, error: 'empty' };
70
- /* fold'lu dedup: aynı/near-same kayıt iki kez girmez */
71
- const key = fold(t).replace(/[^a-z0-9]+/g, ' ').trim();
72
- const list = entries();
73
- if (list.some((l) => fold(l).replace(/[^a-z0-9]+/g, ' ').trim() === key)) {
74
- return { ok: true, duplicate: true };
75
- }
76
- fs.appendFileSync(path.join(memDir(), 'MEMORY.md'), '- ' + t + '\n');
77
- /* cap koruması: 400 kaydı aşınca otomatik hijyen */
78
- if (list.length + 1 > 400) {
79
- try { hygiene({}); } catch {}
80
- }
81
- return { ok: true };
82
- } catch (e) {
83
- return { ok: false, error: String((e && e.message) || e) };
84
- }
85
- }
86
-
87
- /* USER.md: kullanıcının PROİL dosyası (ad, hitap, tercihler, projeler).
88
- append'in satır-satıır dedup'ı burada da geçerli; kayıt sayısı sınırlı tutulur. */
89
- const USER_CAP = 60;
90
-
91
- function appendUser(text) {
92
- try {
93
- ensure();
94
- const t = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 300);
95
- if (!t) return { ok: false, error: 'empty' };
96
- const lines = read('USER.md')
97
- .split('\n')
98
- .map((l) => l.replace(/^[-*]\s*/, '').trim())
99
- .filter(Boolean);
100
- /* profil anahtarı: "Konu: ..." satırlarında Konu — "Hitap: kanka" sonra
101
- "Hitap: Batuhan" gelirse satır eklenmez, eskisi GÜNCELLENİR */
102
- const topicOf = (s) => {
103
- const c = fold(s).indexOf(':');
104
- return (c > 0 ? fold(s).slice(0, c) : fold(s)).replace(/[^a-z0-9]+/g, ' ').trim();
105
- };
106
- const topic = topicOf(t);
107
- const idx = lines.findIndex((l) => topicOf(l) === topic);
108
- if (idx >= 0) {
109
- if (lines[idx] === t) return { ok: true, duplicate: true };
110
- lines[idx] = t;
111
- } else {
112
- lines.push(t);
113
- }
114
- while (lines.length > USER_CAP) lines.shift();
115
- fs.writeFileSync(path.join(memDir(), 'USER.md'), lines.map((l) => '- ' + l).join('\n') + '\n');
116
- return { ok: true, updated: idx >= 0 };
117
- } catch (e) {
118
- return { ok: false, error: String((e && e.message) || e) };
119
- }
120
- }
121
-
122
- const ALLOWED_FILES = ['SOUL.md', 'MEMORY.md', 'USER.md'];
123
-
124
- function save(file, content) {
125
- try {
126
- if (!ALLOWED_FILES.includes(file)) return { ok: false, error: 'bad file' };
127
- ensure();
128
- fs.writeFileSync(path.join(memDir(), file), String(content ?? ''), 'utf8');
129
- /* UI/admin MEMORY.md'yi elle değiştirdiyse mem0 store'unu satırlardan yeniden kur
130
- (store = arama doğruluk kaynağı; ayna dosyası kullanıcıya görünür kalır) */
131
- if (file === 'MEMORY.md') {
132
- try {
133
- const mem0 = require('./mem0');
134
- mem0.reindexFromLines('main', String(content ?? '').split('\n').map((l) => l.replace(/^[-*]\s*/, '').trim()).filter(Boolean));
135
- } catch {}
136
- }
137
- return { ok: true };
138
- } catch (e) {
139
- return { ok: false, error: String((e && e.message) || e) };
140
- }
141
- }
142
-
143
- /* ---------- retrieval ---------- */
144
-
145
- const TR_FOLD = { ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u' };
146
-
147
- function fold(s) {
148
- return String(s || '')
149
- .toLowerCase()
150
- .replace(/[çğıöşüâîû]/g, (ch) => TR_FOLD[ch] || ch);
151
- }
152
-
153
- function tokenize(q) {
154
- return (fold(q).match(/[a-z0-9_+#.]+/g) || []).filter((w) => w.length >= 3);
155
- }
156
-
157
- function entries() {
158
- return read('MEMORY.md')
159
- .split('\n')
160
- .map((l) => l.replace(/^[-*]\s*/, '').trim())
161
- .filter(Boolean);
162
- }
163
-
164
- function scoreEntry(entryText, qTokens) {
165
- if (!qTokens.length) return 0;
166
- const hay = fold(entryText);
167
- let hits = 0;
168
- for (const t of new Set(qTokens)) {
169
- if (hay.includes(t)) hits++;
170
- }
171
- return hits / Math.sqrt(qTokens.length);
172
- }
173
-
174
- /* Sorguyla ilgili en güçlü N kayıt + en son eklenen M kayıt (sıra korunur). */
175
- function relevantFor(query, { maxRelevant = 6, maxRecent = 4, charCap = 2400 } = {}) {
176
- const list = entries();
177
- if (!list.length) return '';
178
- const qTokens = tokenize(query);
179
- const picked = new Set();
180
- if (qTokens.length) {
181
- const scored = list
182
- .map((text, i) => ({ i, text, score: scoreEntry(text, qTokens) }))
183
- .filter((r) => r.score > 0)
184
- .sort((a, b) => b.score - a.score)
185
- .slice(0, maxRelevant);
186
- for (const r of scored) picked.add(r.i);
187
- }
188
- for (let i = list.length - 1; i >= 0 && picked.size < maxRelevant + maxRecent; i--) {
189
- picked.add(i);
190
- }
191
- let out = '';
192
- for (const i of [...picked].sort((a, b) => a - b)) {
193
- const line = '- ' + list[i];
194
- if (out.length + line.length > charCap) break;
195
- out += (out ? '\n' : '') + line;
196
- }
197
- return out;
198
- }
199
-
200
- function search(query, limit = 8) {
201
- const list = entries();
202
- const qTokens = tokenize(query);
203
- if (!list.length) return [];
204
- let rows;
205
- if (!qTokens.length) {
206
- rows = list.map((text, i) => ({ text, score: 0, i })).slice(-limit).reverse();
207
- } else {
208
- rows = list
209
- .map((text, i) => ({ text, score: Number(scoreEntry(text, qTokens).toFixed(3)), i }))
210
- .filter((r) => r.score > 0)
211
- .sort((a, b) => b.score - a.score)
212
- .slice(0, limit);
213
- }
214
- return rows.map(({ text, score }) => ({ score, text }));
215
- }
216
-
217
- /* ---------- kalıcı kurallar (#3 düzeltme hattı) ----------
218
- Kullanıcının agent'a söylediği "artık hep böyle yap" maddeleri.
219
- MEMORY.md'yi şişirmeden ayrı dosyada durur, her prompta girer. */
220
-
221
- function rulesFile() {
222
- return path.join(beastRoot(), 'rules.md');
223
- }
224
-
225
- function listRules() {
226
- try {
227
- return fs
228
- .readFileSync(rulesFile(), 'utf8')
229
- .split('\n')
230
- .map((l) => l.replace(/^-\s*/, '').trim())
231
- .filter(Boolean);
232
- } catch {
233
- return [];
234
- }
235
- }
236
-
237
- function addRule(text) {
238
- const t = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 300);
239
- if (!t) return { ok: false, error: 'kural metni boş' };
240
- if (listRules().some((r) => r.toLowerCase() === t.toLowerCase())) {
241
- return { ok: true, duplicate: true };
242
- }
243
- try {
244
- fs.mkdirSync(beastRoot(), { recursive: true });
245
- fs.appendFileSync(rulesFile(), '- ' + t + '\n');
246
- return { ok: true };
247
- } catch (e) {
248
- return { ok: false, error: String((e && e.message) || e) };
249
- }
250
- }
251
-
252
- function removeRule(textOrIndex) {
253
- const rules = listRules();
254
- let idx = -1;
255
- if (/^\d+$/.test(String(textOrIndex))) idx = Number(textOrIndex) - 1;
256
- else idx = rules.findIndex((r) => r.toLowerCase() === String(textOrIndex).trim().toLowerCase());
257
- if (idx < 0 || idx >= rules.length) return { ok: false, error: 'kural bulunamadı' };
258
- rules.splice(idx, 1);
259
- try {
260
- fs.writeFileSync(rulesFile(), rules.map((r) => '- ' + r).join('\n') + (rules.length ? '\n' : ''));
261
- return { ok: true };
262
- } catch (e) {
263
- return { ok: false, error: String((e && e.message) || e) };
264
- }
265
- }
266
-
267
- /* ---------- bellek hijyeni (#5) ----------
268
- MEMORY.md: dedup + eskime; periyodik çağrıyla şişme engellenir. */
269
-
270
- const MEMORY_CAP = 400; // maksimum kayıt sayısı
271
- const MEMORY_MAX_AGE_DAYS = 120; // hiç skor üretmeyen kayıtların ömrü
272
-
273
- /** Duplike + çok eski kayıtları temizler; silinen sayısını döner */
274
- function hygiene({ maxAgeDays = MEMORY_MAX_AGE_DAYS, cap = MEMORY_CAP } = {}) {
275
- const list = entries();
276
- if (!list.length) return { ok: true, removed: 0, reason: 'boş' };
277
- const seen = new Set();
278
- const kept = [];
279
- let dupes = 0;
280
- for (const l of list) {
281
- const key = fold(l).replace(/[^a-z0-9]+/g, ' ').trim();
282
- if (seen.has(key)) {
283
- dupes++;
284
- continue;
285
- }
286
- seen.add(key);
287
- kept.push(l);
288
- }
289
- let aged = 0;
290
- if (kept.length > cap || maxAgeDays > 0) {
291
- /* tarih etiketli olmayanlar en eski kabul edilir (append-only başlangıç) */
292
- const cut = Date.now() - maxAgeDays * 86400000;
293
- const filtered = [];
294
- /* kronolojik varsayım: dosya sırası = ekleme sırası */
295
- const overflow = Math.max(0, kept.length - cap);
296
- kept.forEach((l, idx) => {
297
- const m = l.match(/\b(\d{4}-\d{2}-\d{2})\b/);
298
- const tooOld = m && new Date(m[1]).getTime() < cut;
299
- if (tooOld && idx < kept.length - 20) { // son 20 kayıt asla yaşla silinmez
300
- aged++;
301
- return;
302
- }
303
- if (overflow > 0 && idx < overflow) {
304
- // kap aşımı: en eskilerden düş ama yine de son 20 korunur
305
- if (idx < kept.length - 20) {
306
- aged++;
307
- return;
308
- }
309
- }
310
- filtered.push(l);
311
- });
312
- return writeKept(filtered, dupes + aged);
313
- }
314
- return writeKept(kept, dupes);
315
- }
316
-
317
- function writeKept(kept, removedCount) {
318
- try {
319
- fs.writeFileSync(
320
- path.join(memDir(), 'MEMORY.md'),
321
- kept.map((l) => '- ' + l).join('\n') + (kept.length ? '\n' : '')
322
- );
323
- return { ok: true, removed: removedCount, remaining: kept.length };
324
- } catch (e) {
325
- return { ok: false, error: String((e && e.message) || e) };
326
- }
327
- }
328
-
329
- /* ---------- #12 kişilik kalibrasyonu ----------
330
- Kullanıcının konuşma tarzından üslup imzası çıkarır, SOUL.md'ye ekler.
331
- İlk hafta mesajlar birikince "dijital ikiz" ton yakalanır. */
332
-
333
- function personaFile() {
334
- return path.join(memDir(), 'persona.json');
335
- }
336
-
337
- function loadPersona() {
338
- try {
339
- return JSON.parse(fs.readFileSync(personaFile(), 'utf8'));
340
- } catch {
341
- return { samples: [], calibrated: false };
342
- }
343
- }
344
-
345
- function savePersona(p) {
346
- try {
347
- fs.mkdirSync(memDir(), { recursive: true });
348
- fs.writeFileSync(personaFile(), JSON.stringify(p, null, 2));
349
- } catch {}
350
- }
351
-
352
- /** Kullanıcı mesajı → üslup örnek havuzuna ekle (max 40) */
353
- function addStyleSample(text) {
354
- const t = String(text || '').trim();
355
- if (t.length < 8 || t.length > 300) return; // çok kısa/uzun örnek değmez
356
- if (/^(\/|http)/.test(t)) return; // komut/link değil
357
- const p = loadPersona();
358
- if (p.samples.some((s) => s.toLowerCase() === t.toLowerCase())) return;
359
- p.samples.push(t);
360
- if (p.samples.length > 40) p.samples = p.samples.slice(-40);
361
- savePersona(p);
362
- }
363
-
364
- const STYLE_HINTS = [
365
- ['emoji', /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u],
366
- ['kısa cümle', /^(.{1,60}(\.|$)){1,2}/],
367
- ['türkçe kısaltma', /\b(tmm|tamam|tamamsa|olur|olmz|yok|bşka|hemen|acele|asap)\b/i],
368
- ['seslenme', /\b(kanka|abi|abey|hocam|kanka)\b/i],
369
- ['üzgün ünlem', /!{1,}$/m],
370
- ['soru ağırlıklı', /\?/],
371
- ];
372
-
373
- function analyzeStyle(samples) {
374
- const counts = {};
375
- for (const [, re] of STYLE_HINTS) {
376
- for (const s of samples) if (re.test(s)) counts[re.source] = (counts[re.source] || 0) + 1;
377
- }
378
- // basit: hangi hintler %35+ örnekte görünüyor
379
- const n = Math.max(1, samples.length);
380
- return STYLE_HINTS.filter(([, re]) => (counts[re.source] || 0) / n >= 0.35).map(([name]) => name);
381
- }
382
-
383
- /** Yeterli örnek biriktiyse SOUL.md'ye üslup paragrafı yaz/revize et */
384
- function calibratePersona() {
385
- const p = loadPersona();
386
- if (p.samples.length < 20) return { ok: false, reason: 'yetersiz örnek', have: p.samples.length, need: 20 };
387
- const traits = analyzeStyle(p.samples);
388
- const style =
389
- '# KULLANICI ÜSLUBU (sohbetlerinden kalibre edildi — bu tonda konuş)\n' +
390
- (traits.length
391
- ? '- Gözlemlenen özellikler: ' + traits.join(', ') + '\n'
392
- : '- Nötr ve ölçülü bir üslup\n') +
393
- '- Örnek kullanıcı cümleleri:\n' +
394
- p.samples.slice(-6).map((s) => ' • "' + s.slice(0, 90) + '"').join('\n');
395
- try {
396
- const soulPath = path.join(memDir(), 'SOUL.md');
397
- let soul = fs.readFileSync(soulPath, 'utf8');
398
- soul = soul.replace(/# KULLANICI ÜSLUBU[\s\S]*?(?=\n# |\n*$)/g, '').trimEnd();
399
- fs.writeFileSync(soulPath, soul + '\n\n' + style + '\n');
400
- p.calibrated = true;
401
- p.calibratedAt = new Date().toISOString();
402
- savePersona(p);
403
- return { ok: true, traits };
404
- } catch (e) {
405
- return { ok: false, error: String((e && e.message) || e) };
406
- }
407
- }
408
-
409
- module.exports = {
410
- beastRoot,
411
- memDir,
412
- loadAll,
413
- append,
414
- appendUser,
415
- save,
416
- entries,
417
- relevantFor,
418
- search,
419
- tokenize,
420
- scoreEntry,
421
- listRules,
422
- addRule,
423
- removeRule,
424
- hygiene,
425
- addStyleSample,
426
- calibratePersona,
427
- };
1
+ 'use strict';
2
+
3
+ /* Beast memory: SOUL.md + MEMORY.md + USER.md under %APPDATA%\beast\memories.
4
+ Tiny and append-only; the model writes via the memory_write tool.
5
+ MEMORY.md is NOT dumped into the prompt anymore: buildSystem pulls only
6
+ entries relevant to the current query (retrieval) and the agent can call
7
+ memory_search to dig deeper on demand. */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ function beastRoot() {
14
+ if (process.env.BEAST_DATA) return process.env.BEAST_DATA; // test/taşınabilirlik
15
+ return process.env.APPDATA
16
+ ? path.join(process.env.APPDATA, 'beast')
17
+ : path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
18
+ }
19
+
20
+ function memDir() {
21
+ return path.join(beastRoot(), 'memories');
22
+ }
23
+
24
+ function ensure() {
25
+ fs.mkdirSync(memDir(), { recursive: true });
26
+ const soulPath = path.join(memDir(), 'SOUL.md');
27
+ if (!fs.existsSync(soulPath)) {
28
+ /* SOUL.md = ajanın kişiliği. Kullanıcı Ayarlar → Memory'den düzenler;
29
+ bu dosyanın tamamı her istekte sistem promptuna birebir girer. */
30
+ const seed = [
31
+ 'Sen Beast Agent\u2019sın — kullanıcının Windows makinesinde yerel çalışan hızlı, hafif ve becerikli bir asistan.',
32
+ '',
33
+ 'KİŞİLİK',
34
+ '- Doğrudan ve netsin; lafı dolandırmazsın, gereksiz özür ve dolgu cümle kurmazsın.',
35
+ '- Samimi ama işini ciddiye alırsın; kullanıcıya güven verirsin.',
36
+ '- Proaktiftir: yapabildiğini yapar, mümkünse sormak yerine yapar.',
37
+ '',
38
+ 'TON VE DAVRANIŞ',
39
+ '- Kısa ve öz cevap ver; madde işaretlerinde "-" kullan.',
40
+ '- Yaptığın işleri kısaca raporlarsın; hata olursa nasıl düzeltileceğini da söylersin.',
41
+ '',
42
+ '(Kişiliğini değiştirmek için bu dosyayı düzenle — yazdığın her şey ajanı tanımlar.)',
43
+ ].join('\n');
44
+ fs.writeFileSync(soulPath, seed);
45
+ }
46
+ for (const f of ['MEMORY.md', 'USER.md']) {
47
+ const p = path.join(memDir(), f);
48
+ if (!fs.existsSync(p)) fs.writeFileSync(p, '');
49
+ }
50
+ }
51
+
52
+ function read(f) {
53
+ try {
54
+ return fs.readFileSync(path.join(memDir(), f), 'utf8').trim();
55
+ } catch {
56
+ return '';
57
+ }
58
+ }
59
+
60
+ function loadAll() {
61
+ ensure();
62
+ return { soul: read('SOUL.md'), user: read('USER.md'), memory: read('MEMORY.md') };
63
+ }
64
+
65
+ function append(text) {
66
+ try {
67
+ ensure();
68
+ const t = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 500);
69
+ if (!t) return { ok: false, error: 'empty' };
70
+ /* fold'lu dedup: aynı/near-same kayıt iki kez girmez */
71
+ const key = fold(t).replace(/[^a-z0-9]+/g, ' ').trim();
72
+ const list = entries();
73
+ if (list.some((l) => fold(l).replace(/[^a-z0-9]+/g, ' ').trim() === key)) {
74
+ return { ok: true, duplicate: true };
75
+ }
76
+ fs.appendFileSync(path.join(memDir(), 'MEMORY.md'), '- ' + t + '\n');
77
+ /* cap koruması: 400 kaydı aşınca otomatik hijyen */
78
+ if (list.length + 1 > 400) {
79
+ try { hygiene({}); } catch {}
80
+ }
81
+ return { ok: true };
82
+ } catch (e) {
83
+ return { ok: false, error: String((e && e.message) || e) };
84
+ }
85
+ }
86
+
87
+ /* USER.md: kullanıcının PROİL dosyası (ad, hitap, tercihler, projeler).
88
+ append'in satır-satıır dedup'ı burada da geçerli; kayıt sayısı sınırlı tutulur. */
89
+ const USER_CAP = 60;
90
+
91
+ function appendUser(text) {
92
+ try {
93
+ ensure();
94
+ const t = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 300);
95
+ if (!t) return { ok: false, error: 'empty' };
96
+ const lines = read('USER.md')
97
+ .split('\n')
98
+ .map((l) => l.replace(/^[-*]\s*/, '').trim())
99
+ .filter(Boolean);
100
+ /* profil anahtarı: "Konu: ..." satırlarında Konu — "Hitap: kanka" sonra
101
+ "Hitap: Batuhan" gelirse satır eklenmez, eskisi GÜNCELLENİR */
102
+ const topicOf = (s) => {
103
+ const c = fold(s).indexOf(':');
104
+ return (c > 0 ? fold(s).slice(0, c) : fold(s)).replace(/[^a-z0-9]+/g, ' ').trim();
105
+ };
106
+ const topic = topicOf(t);
107
+ const idx = lines.findIndex((l) => topicOf(l) === topic);
108
+ if (idx >= 0) {
109
+ if (lines[idx] === t) return { ok: true, duplicate: true };
110
+ lines[idx] = t;
111
+ } else {
112
+ lines.push(t);
113
+ }
114
+ while (lines.length > USER_CAP) lines.shift();
115
+ fs.writeFileSync(path.join(memDir(), 'USER.md'), lines.map((l) => '- ' + l).join('\n') + '\n');
116
+ return { ok: true, updated: idx >= 0 };
117
+ } catch (e) {
118
+ return { ok: false, error: String((e && e.message) || e) };
119
+ }
120
+ }
121
+
122
+ const ALLOWED_FILES = ['SOUL.md', 'MEMORY.md', 'USER.md'];
123
+
124
+ function save(file, content) {
125
+ try {
126
+ if (!ALLOWED_FILES.includes(file)) return { ok: false, error: 'bad file' };
127
+ ensure();
128
+ fs.writeFileSync(path.join(memDir(), file), String(content ?? ''), 'utf8');
129
+ /* UI/admin MEMORY.md'yi elle değiştirdiyse mem0 store'unu satırlardan yeniden kur
130
+ (store = arama doğruluk kaynağı; ayna dosyası kullanıcıya görünür kalır) */
131
+ if (file === 'MEMORY.md') {
132
+ try {
133
+ const mem0 = require('./mem0');
134
+ mem0.reindexFromLines('main', String(content ?? '').split('\n').map((l) => l.replace(/^[-*]\s*/, '').trim()).filter(Boolean));
135
+ } catch {}
136
+ }
137
+ return { ok: true };
138
+ } catch (e) {
139
+ return { ok: false, error: String((e && e.message) || e) };
140
+ }
141
+ }
142
+
143
+ /* ---------- retrieval ---------- */
144
+
145
+ const TR_FOLD = { ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u' };
146
+
147
+ function fold(s) {
148
+ return String(s || '')
149
+ .toLowerCase()
150
+ .replace(/[çğıöşüâîû]/g, (ch) => TR_FOLD[ch] || ch);
151
+ }
152
+
153
+ function tokenize(q) {
154
+ return (fold(q).match(/[a-z0-9_+#.]+/g) || []).filter((w) => w.length >= 3);
155
+ }
156
+
157
+ function entries() {
158
+ return read('MEMORY.md')
159
+ .split('\n')
160
+ .map((l) => l.replace(/^[-*]\s*/, '').trim())
161
+ .filter(Boolean);
162
+ }
163
+
164
+ function scoreEntry(entryText, qTokens) {
165
+ if (!qTokens.length) return 0;
166
+ const hay = fold(entryText);
167
+ let hits = 0;
168
+ for (const t of new Set(qTokens)) {
169
+ if (hay.includes(t)) hits++;
170
+ }
171
+ return hits / Math.sqrt(qTokens.length);
172
+ }
173
+
174
+ /* Sorguyla ilgili en güçlü N kayıt + en son eklenen M kayıt (sıra korunur). */
175
+ function relevantFor(query, { maxRelevant = 6, maxRecent = 4, charCap = 2400 } = {}) {
176
+ const list = entries();
177
+ if (!list.length) return '';
178
+ const qTokens = tokenize(query);
179
+ const picked = new Set();
180
+ if (qTokens.length) {
181
+ const scored = list
182
+ .map((text, i) => ({ i, text, score: scoreEntry(text, qTokens) }))
183
+ .filter((r) => r.score > 0)
184
+ .sort((a, b) => b.score - a.score)
185
+ .slice(0, maxRelevant);
186
+ for (const r of scored) picked.add(r.i);
187
+ }
188
+ for (let i = list.length - 1; i >= 0 && picked.size < maxRelevant + maxRecent; i--) {
189
+ picked.add(i);
190
+ }
191
+ let out = '';
192
+ for (const i of [...picked].sort((a, b) => a - b)) {
193
+ const line = '- ' + list[i];
194
+ if (out.length + line.length > charCap) break;
195
+ out += (out ? '\n' : '') + line;
196
+ }
197
+ return out;
198
+ }
199
+
200
+ function search(query, limit = 8) {
201
+ const list = entries();
202
+ const qTokens = tokenize(query);
203
+ if (!list.length) return [];
204
+ let rows;
205
+ if (!qTokens.length) {
206
+ rows = list.map((text, i) => ({ text, score: 0, i })).slice(-limit).reverse();
207
+ } else {
208
+ rows = list
209
+ .map((text, i) => ({ text, score: Number(scoreEntry(text, qTokens).toFixed(3)), i }))
210
+ .filter((r) => r.score > 0)
211
+ .sort((a, b) => b.score - a.score)
212
+ .slice(0, limit);
213
+ }
214
+ return rows.map(({ text, score }) => ({ score, text }));
215
+ }
216
+
217
+ /* ---------- kalıcı kurallar (#3 düzeltme hattı) ----------
218
+ Kullanıcının agent'a söylediği "artık hep böyle yap" maddeleri.
219
+ MEMORY.md'yi şişirmeden ayrı dosyada durur, her prompta girer. */
220
+
221
+ function rulesFile() {
222
+ return path.join(beastRoot(), 'rules.md');
223
+ }
224
+
225
+ function listRules() {
226
+ try {
227
+ return fs
228
+ .readFileSync(rulesFile(), 'utf8')
229
+ .split('\n')
230
+ .map((l) => l.replace(/^-\s*/, '').trim())
231
+ .filter(Boolean);
232
+ } catch {
233
+ return [];
234
+ }
235
+ }
236
+
237
+ function addRule(text) {
238
+ const t = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 300);
239
+ if (!t) return { ok: false, error: 'kural metni boş' };
240
+ if (listRules().some((r) => r.toLowerCase() === t.toLowerCase())) {
241
+ return { ok: true, duplicate: true };
242
+ }
243
+ try {
244
+ fs.mkdirSync(beastRoot(), { recursive: true });
245
+ fs.appendFileSync(rulesFile(), '- ' + t + '\n');
246
+ return { ok: true };
247
+ } catch (e) {
248
+ return { ok: false, error: String((e && e.message) || e) };
249
+ }
250
+ }
251
+
252
+ function removeRule(textOrIndex) {
253
+ const rules = listRules();
254
+ let idx = -1;
255
+ if (/^\d+$/.test(String(textOrIndex))) idx = Number(textOrIndex) - 1;
256
+ else idx = rules.findIndex((r) => r.toLowerCase() === String(textOrIndex).trim().toLowerCase());
257
+ if (idx < 0 || idx >= rules.length) return { ok: false, error: 'kural bulunamadı' };
258
+ rules.splice(idx, 1);
259
+ try {
260
+ fs.writeFileSync(rulesFile(), rules.map((r) => '- ' + r).join('\n') + (rules.length ? '\n' : ''));
261
+ return { ok: true };
262
+ } catch (e) {
263
+ return { ok: false, error: String((e && e.message) || e) };
264
+ }
265
+ }
266
+
267
+ /* ---------- bellek hijyeni (#5) ----------
268
+ MEMORY.md: dedup + eskime; periyodik çağrıyla şişme engellenir. */
269
+
270
+ const MEMORY_CAP = 400; // maksimum kayıt sayısı
271
+ const MEMORY_MAX_AGE_DAYS = 120; // hiç skor üretmeyen kayıtların ömrü
272
+
273
+ /** Duplike + çok eski kayıtları temizler; silinen sayısını döner */
274
+ function hygiene({ maxAgeDays = MEMORY_MAX_AGE_DAYS, cap = MEMORY_CAP } = {}) {
275
+ const list = entries();
276
+ if (!list.length) return { ok: true, removed: 0, reason: 'boş' };
277
+ const seen = new Set();
278
+ const kept = [];
279
+ let dupes = 0;
280
+ for (const l of list) {
281
+ const key = fold(l).replace(/[^a-z0-9]+/g, ' ').trim();
282
+ if (seen.has(key)) {
283
+ dupes++;
284
+ continue;
285
+ }
286
+ seen.add(key);
287
+ kept.push(l);
288
+ }
289
+ let aged = 0;
290
+ if (kept.length > cap || maxAgeDays > 0) {
291
+ /* tarih etiketli olmayanlar en eski kabul edilir (append-only başlangıç) */
292
+ const cut = Date.now() - maxAgeDays * 86400000;
293
+ const filtered = [];
294
+ /* kronolojik varsayım: dosya sırası = ekleme sırası */
295
+ const overflow = Math.max(0, kept.length - cap);
296
+ kept.forEach((l, idx) => {
297
+ const m = l.match(/\b(\d{4}-\d{2}-\d{2})\b/);
298
+ const tooOld = m && new Date(m[1]).getTime() < cut;
299
+ if (tooOld && idx < kept.length - 20) { // son 20 kayıt asla yaşla silinmez
300
+ aged++;
301
+ return;
302
+ }
303
+ if (overflow > 0 && idx < overflow) {
304
+ // kap aşımı: en eskilerden düş ama yine de son 20 korunur
305
+ if (idx < kept.length - 20) {
306
+ aged++;
307
+ return;
308
+ }
309
+ }
310
+ filtered.push(l);
311
+ });
312
+ return writeKept(filtered, dupes + aged);
313
+ }
314
+ return writeKept(kept, dupes);
315
+ }
316
+
317
+ function writeKept(kept, removedCount) {
318
+ try {
319
+ fs.writeFileSync(
320
+ path.join(memDir(), 'MEMORY.md'),
321
+ kept.map((l) => '- ' + l).join('\n') + (kept.length ? '\n' : '')
322
+ );
323
+ return { ok: true, removed: removedCount, remaining: kept.length };
324
+ } catch (e) {
325
+ return { ok: false, error: String((e && e.message) || e) };
326
+ }
327
+ }
328
+
329
+ /* ---------- #12 kişilik kalibrasyonu ----------
330
+ Kullanıcının konuşma tarzından üslup imzası çıkarır, SOUL.md'ye ekler.
331
+ İlk hafta mesajlar birikince "dijital ikiz" ton yakalanır. */
332
+
333
+ function personaFile() {
334
+ return path.join(memDir(), 'persona.json');
335
+ }
336
+
337
+ function loadPersona() {
338
+ try {
339
+ return JSON.parse(fs.readFileSync(personaFile(), 'utf8'));
340
+ } catch {
341
+ return { samples: [], calibrated: false };
342
+ }
343
+ }
344
+
345
+ function savePersona(p) {
346
+ try {
347
+ fs.mkdirSync(memDir(), { recursive: true });
348
+ fs.writeFileSync(personaFile(), JSON.stringify(p, null, 2));
349
+ } catch {}
350
+ }
351
+
352
+ /** Kullanıcı mesajı → üslup örnek havuzuna ekle (max 40) */
353
+ function addStyleSample(text) {
354
+ const t = String(text || '').trim();
355
+ if (t.length < 8 || t.length > 300) return; // çok kısa/uzun örnek değmez
356
+ if (/^(\/|http)/.test(t)) return; // komut/link değil
357
+ const p = loadPersona();
358
+ if (p.samples.some((s) => s.toLowerCase() === t.toLowerCase())) return;
359
+ p.samples.push(t);
360
+ if (p.samples.length > 40) p.samples = p.samples.slice(-40);
361
+ savePersona(p);
362
+ }
363
+
364
+ const STYLE_HINTS = [
365
+ ['emoji', /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u],
366
+ ['kısa cümle', /^(.{1,60}(\.|$)){1,2}/],
367
+ ['türkçe kısaltma', /\b(tmm|tamam|tamamsa|olur|olmz|yok|bşka|hemen|acele|asap)\b/i],
368
+ ['seslenme', /\b(kanka|abi|abey|hocam|kanka)\b/i],
369
+ ['üzgün ünlem', /!{1,}$/m],
370
+ ['soru ağırlıklı', /\?/],
371
+ ];
372
+
373
+ function analyzeStyle(samples) {
374
+ const counts = {};
375
+ for (const [, re] of STYLE_HINTS) {
376
+ for (const s of samples) if (re.test(s)) counts[re.source] = (counts[re.source] || 0) + 1;
377
+ }
378
+ // basit: hangi hintler %35+ örnekte görünüyor
379
+ const n = Math.max(1, samples.length);
380
+ return STYLE_HINTS.filter(([, re]) => (counts[re.source] || 0) / n >= 0.35).map(([name]) => name);
381
+ }
382
+
383
+ /** Yeterli örnek biriktiyse SOUL.md'ye üslup paragrafı yaz/revize et */
384
+ function calibratePersona() {
385
+ const p = loadPersona();
386
+ if (p.samples.length < 20) return { ok: false, reason: 'yetersiz örnek', have: p.samples.length, need: 20 };
387
+ const traits = analyzeStyle(p.samples);
388
+ const style =
389
+ '# KULLANICI ÜSLUBU (sohbetlerinden kalibre edildi — bu tonda konuş)\n' +
390
+ (traits.length
391
+ ? '- Gözlemlenen özellikler: ' + traits.join(', ') + '\n'
392
+ : '- Nötr ve ölçülü bir üslup\n') +
393
+ '- Örnek kullanıcı cümleleri:\n' +
394
+ p.samples.slice(-6).map((s) => ' • "' + s.slice(0, 90) + '"').join('\n');
395
+ try {
396
+ const soulPath = path.join(memDir(), 'SOUL.md');
397
+ let soul = fs.readFileSync(soulPath, 'utf8');
398
+ soul = soul.replace(/# KULLANICI ÜSLUBU[\s\S]*?(?=\n# |\n*$)/g, '').trimEnd();
399
+ fs.writeFileSync(soulPath, soul + '\n\n' + style + '\n');
400
+ p.calibrated = true;
401
+ p.calibratedAt = new Date().toISOString();
402
+ savePersona(p);
403
+ return { ok: true, traits };
404
+ } catch (e) {
405
+ return { ok: false, error: String((e && e.message) || e) };
406
+ }
407
+ }
408
+
409
+ module.exports = {
410
+ beastRoot,
411
+ memDir,
412
+ loadAll,
413
+ append,
414
+ appendUser,
415
+ save,
416
+ entries,
417
+ relevantFor,
418
+ search,
419
+ tokenize,
420
+ scoreEntry,
421
+ listRules,
422
+ addRule,
423
+ removeRule,
424
+ hygiene,
425
+ addStyleSample,
426
+ calibratePersona,
427
+ };