beast-agent 1.9.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 -130
  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 +75 -10
  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 -427
  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 -652
  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 +348 -6
  47. package/src/preload.js +9 -0
  48. package/src/renderer/browserPreload.js +73 -73
  49. package/src/renderer/i18n.js +4 -0
  50. package/src/renderer/index.html +448 -409
  51. package/src/renderer/renderer.js +824 -41
  52. package/src/renderer/style.css +264 -5
package/src/agent/mem0.js CHANGED
@@ -1,605 +1,605 @@
1
- 'use strict';
2
-
3
- /* mem0-native: mem0 mimarisinin (github.com/mem0ai/mem0) Beast'e uyarlanmış
4
- Node çekirdeği. Python/mem0-ts BAĞIMLILIĞI YOK:
5
- - Vektör: @xenova/transformers (whisper için zaten var) → all-MiniLM-L6-v2 (384d)
6
- - Store: JSON dosya per scope → bot başına AYRI dosya = tam izolasyon
7
- - Konsolidasyon: mem0 klasik update prompt'u → ADD / UPDATE / DELETE / NONE
8
- - Audit: JSONL geçmiş (mem0 history.db karşılığı)
9
- - Hibrit arama: 0.65*semantic(cosine) + 0.35*keyword (mem0 semantic+BM25 felsefesi)
10
- Her fonksiyon degradasyonlu: embedding yoksa keyword, LLM yoksa hash+cos dedup.
11
- Scope: 'main' | 'bot:<botId>' — botlar arasında ASLA çapraz erişim yok. */
12
-
13
- const fs = require('fs');
14
- const path = require('path');
15
- const os = require('os');
16
- const crypto = require('crypto');
17
-
18
- const CAP = 400; // scope başına maksimum kayıt (memory.js cap ile aynı)
19
- const DIMS = 384; // all-MiniLM-L6-v2
20
- const SEMANTIC_SKIP = 0.93; // add fast-path: bu cosine üstü aynı bilgi sayılır
21
- const CAND_THRESHOLD = 0.3; // konsolidasyon adayı için min cosine
22
- const CAND_K = 6; // fact başına çekilecek benzer aday
23
- const SEARCH_SEM_W = 0.65; // hibrit skor: semantik ağırlık
24
- const SEARCH_KW_W = 0.35; // hibrit skor: keyword ağırlık
25
- const LOAD_TIMEOUT_MS = 10000; // embed modeli ilk yüklemeye bu süreyi bekler, sonra vektörsüz devam
26
-
27
- /* ---------- yollar ---------- */
28
-
29
- function beastRoot() {
30
- if (process.env.BEAST_DATA) return process.env.BEAST_DATA;
31
- return process.env.APPDATA
32
- ? path.join(process.env.APPDATA, 'beast')
33
- : path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
34
- }
35
-
36
- function mem0Dir() {
37
- const d = path.join(beastRoot(), 'memories', 'mem0');
38
- try { fs.mkdirSync(d, { recursive: true }); } catch {}
39
- return d;
40
- }
41
-
42
- /* scope → güvenli dosya adı ('bot:ahmet-42' → 'bot-ahmet-42') */
43
- function fileSafe(scope) {
44
- return String(scope || 'main').replace(/[^a-zA-Z0-9._-]+/g, '-');
45
- }
46
-
47
- function storePath(scope) {
48
- return path.join(mem0Dir(), fileSafe(scope) + '.json');
49
- }
50
-
51
- function logPath(scope) {
52
- return path.join(mem0Dir(), fileSafe(scope) + '.log');
53
- }
54
-
55
- /* ---------- metin skorlama (memory.js ile aynı klasikler — öz-bağımsız) ---------- */
56
-
57
- const TR_FOLD = { ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u' };
58
-
59
- function fold(s) {
60
- return String(s || '')
61
- .toLowerCase()
62
- .replace(/[çğıöşüâîû]/g, (ch) => TR_FOLD[ch] || ch);
63
- }
64
-
65
- function tokenize(q) {
66
- return (fold(q).match(/[a-z0-9_+#.]+/g) || []).filter((w) => w.length >= 3);
67
- }
68
-
69
- function normText(t) {
70
- return String(t || '').replace(/\s+/g, ' ').trim().slice(0, 500);
71
- }
72
-
73
- function md5(s) {
74
- return crypto.createHash('md5').update(String(s || ''), 'utf8').digest('hex');
75
- }
76
-
77
- function scoreEntry(entryText, qTokens) {
78
- if (!qTokens.length) return 0;
79
- const hay = fold(entryText);
80
- let hits = 0;
81
- for (const t of new Set(qTokens)) if (hay.includes(t)) hits++;
82
- return hits / Math.sqrt(qTokens.length);
83
- }
84
-
85
- /* ---------- vektör sağlayıcı ---------- */
86
-
87
- let _embedFn = null; // test enjeksiyonu
88
- let _pipe = null;
89
- let _pipeLoading = null;
90
- let _pipeBroken = false;
91
-
92
- /** Test/ar-ge: harici embedder tak (async (texts) => Float32Array[] | null) */
93
- function setEmbedder(fn) {
94
- _embedFn = typeof fn === 'function' ? fn : null;
95
- }
96
-
97
- function loadPipeline() {
98
- if (_pipe) return Promise.resolve(_pipe);
99
- if (_pipeLoading) return _pipeLoading;
100
- if (_pipeBroken) return Promise.resolve(null);
101
- _pipeLoading = (async () => {
102
- try {
103
- const { pipeline, env } = require('@xenova/transformers');
104
- const modelsDir = process.env.BEAST_MODELS_DIR || path.join(beastRoot(), 'models');
105
- fs.mkdirSync(modelsDir, { recursive: true });
106
- env.cacheDir = modelsDir;
107
- env.allowLocalModels = false;
108
- const p = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { quantized: true });
109
- _pipe = async (texts) => {
110
- const out = [];
111
- for (const t of texts) {
112
- const tensor = await p(t, { pooling: 'mean', normalize: true });
113
- const arr = new Float32Array(DIMS);
114
- const data = tensor.data;
115
- for (let i = 0; i < DIMS && i < data.length; i++) arr[i] = data[i];
116
- out.push(arr);
117
- }
118
- return out;
119
- };
120
- return _pipe;
121
- } catch {
122
- _pipeBroken = true; // bir daha deneme — keyword-only moda düş
123
- return null;
124
- } finally {
125
- _pipeLoading = null;
126
- }
127
- })();
128
- return _pipeLoading;
129
- }
130
-
131
- /* embedder hazırsa döner; ilk yükleme LOAD_TIMEOUT_MS'ı geçerse null (vektörsüz devam) */
132
- async function getEmbedder() {
133
- if (_embedFn) return _embedFn;
134
- const p = loadPipeline();
135
- let timer;
136
- const timeout = new Promise((res) => { timer = setTimeout(() => res(null), LOAD_TIMEOUT_MS); });
137
- const loaded = await Promise.race([p, timeout]).catch(() => null);
138
- clearTimeout(timer);
139
- return loaded || null;
140
- }
141
-
142
- async function embed(texts) {
143
- const fn = await getEmbedder();
144
- if (!fn) return texts.map(() => null);
145
- try {
146
- const vecs = await fn(texts);
147
- if (!Array.isArray(vecs) || vecs.length !== texts.length) return texts.map(() => null);
148
- return vecs.map((v) => (v instanceof Float32Array && v.length === DIMS ? v : null));
149
- } catch {
150
- return texts.map(() => null);
151
- }
152
- }
153
-
154
- function cos(a, b) {
155
- if (!a || !b) return 0;
156
- let dot = 0;
157
- for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
158
- return dot; // normalize vektörler → dot = cosine
159
- }
160
-
161
- function vecToB64(v) {
162
- if (!v) return null;
163
- return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString('base64');
164
- }
165
-
166
- function b64ToVec(s) {
167
- if (!s) return null;
168
- try {
169
- const buf = Buffer.from(String(s), 'base64');
170
- if (buf.length !== DIMS * 4) return null;
171
- return new Float32Array(buf.buffer, buf.byteOffset, DIMS);
172
- } catch {
173
- return null;
174
- }
175
- }
176
-
177
- /* ---------- store ---------- */
178
-
179
- /* mevcut ayna dosyasından satırlar (MEMORY.md) — boot import + admin düzenleme sonrası */
180
- function mirrorLines(scope) {
181
- try {
182
- let p = null;
183
- if (scope === 'main') p = path.join(beastRoot(), 'memories', 'MEMORY.md');
184
- else if (scope.startsWith('bot:')) {
185
- const bots = require('./bots');
186
- p = bots.memPath(scope.slice(4));
187
- }
188
- if (!p) return [];
189
- return fs
190
- .readFileSync(p, 'utf8')
191
- .split('\n')
192
- .map((l) => l.replace(/^[-*]\s*/, '').trim())
193
- .filter(Boolean);
194
- } catch {
195
- return [];
196
- }
197
- }
198
-
199
- function loadStore(scope) {
200
- try {
201
- const raw = fs.readFileSync(storePath(scope), 'utf8');
202
- const data = JSON.parse(raw);
203
- if (data && Array.isArray(data.items)) return data;
204
- } catch {}
205
- /* store dosyası yok → mevcut MEMORY.md satırlarından import (vektörsüz, backfill sonra) */
206
- const items = mirrorLines(scope).map((text) => ({
207
- id: crypto.randomUUID(),
208
- text: normText(text),
209
- hash: md5(normText(text)),
210
- vec: null,
211
- created_at: new Date().toISOString(),
212
- updated_at: new Date().toISOString(),
213
- }));
214
- return { version: 1, items };
215
- }
216
-
217
- function saveStore(scope, store) {
218
- try {
219
- fs.writeFileSync(storePath(scope), JSON.stringify(store), 'utf8');
220
- return true;
221
- } catch {
222
- return false;
223
- }
224
- }
225
-
226
- /* audit günlüğü (mem0 history.db karşılığı — JSONL) */
227
- function audit(scope, entry) {
228
- try {
229
- fs.appendFileSync(logPath(scope), JSON.stringify({ at: new Date().toISOString(), ...entry }) + '\n');
230
- } catch {}
231
- }
232
-
233
- function history(scope, limit = 50) {
234
- try {
235
- const rows = fs
236
- .readFileSync(logPath(scope), 'utf8')
237
- .split('\n')
238
- .filter(Boolean)
239
- .map((l) => {
240
- try { return JSON.parse(l); } catch { return null; }
241
- })
242
- .filter(Boolean);
243
- return rows.slice(-limit).reverse();
244
- } catch {
245
- return [];
246
- }
247
- }
248
-
249
- /* MEMORY.md aynasını store'dan yeniden yaz (store = doğruluk kaynağı) */
250
- function syncMirror(scope) {
251
- try {
252
- const store = loadStore(scope);
253
- const body = store.items.map((it) => '- ' + it.text).join('\n') + (store.items.length ? '\n' : '');
254
- let p = null;
255
- if (scope === 'main') {
256
- const memory = require('./memory');
257
- p = path.join(memory.memDir(), 'MEMORY.md');
258
- } else if (scope.startsWith('bot:')) {
259
- const bots = require('./bots');
260
- p = bots.memPath(scope.slice(4));
261
- }
262
- if (!p) return false;
263
- fs.mkdirSync(path.dirname(p), { recursive: true });
264
- fs.writeFileSync(p, body, 'utf8');
265
- return true;
266
- } catch {
267
- return false;
268
- }
269
- }
270
-
271
- /* admin/UI MEMORY.md'yi elle değiştirdiyse store'u satırlardan yeniden kur */
272
- function reindexFromLines(scope, lines) {
273
- const items = (Array.isArray(lines) ? lines : [])
274
- .map((t) => normText(t))
275
- .filter(Boolean)
276
- .map((text) => ({
277
- id: crypto.randomUUID(),
278
- text,
279
- hash: md5(text),
280
- vec: null,
281
- created_at: new Date().toISOString(),
282
- updated_at: new Date().toISOString(),
283
- }));
284
- const store = { version: 1, items };
285
- saveStore(scope, store);
286
- audit(scope, { event: 'IMPORT', count: items.length });
287
- scheduleBackfill(scope);
288
- return { ok: true, count: items.length };
289
- }
290
-
291
- /* ---------- arka plan vektör doldurma ---------- */
292
-
293
- const _backfillRunning = new Set();
294
-
295
- function scheduleBackfill(scope) {
296
- if (_backfillRunning.has(scope)) return;
297
- _backfillRunning.add(scope);
298
- setTimeout(() => {
299
- _backfill(scope)
300
- .catch(() => {})
301
- .finally(() => _backfillRunning.delete(scope));
302
- }, 1200).unref();
303
- }
304
-
305
- async function _backfill(scope) {
306
- const fn = await getEmbedder();
307
- if (!fn) return;
308
- const store = loadStore(scope);
309
- const todo = store.items.filter((it) => !it.vec).slice(0, 64);
310
- if (!todo.length) return;
311
- const texts = todo.map((it) => it.text);
312
- const vecs = await embed(texts);
313
- todo.forEach((it, i) => {
314
- if (vecs[i]) it.vec = vecToB64(vecs[i]);
315
- });
316
- saveStore(scope, store);
317
- if (store.items.some((it) => !it.vec)) scheduleBackfill(scope);
318
- }
319
-
320
- /* ---------- mem0 klasik konsolidasyon ---------- */
321
-
322
- /* mem0 update prompt'u: yeni fact + benzer mevcut kayıtlar → tek LLM karar çağrısı.
323
- ID'ler indeks ("0","1",...) — LLM'in UUID halüsinasyonuna karşı (mem0 anti-hallucination). */
324
- function buildUpdatePrompt(facts, candidates) {
325
- const sys =
326
- 'Sen bir uzun vadeli hafıza yöneticisisin. Yeni bilgi (fact) ile mevcut kayıtları karşılaştırıp ' +
327
- 'her fact için TEK karar ver. KURALLAR:\n' +
328
- '1. ADD: hafızada olmayan yeni bilgi → {"event":"ADD","text":"<fact>"}\n' +
329
- '2. UPDATE: mevcut kayıtla AYNI konu ama bilgi değişmiş/güncellenmiş → ' +
330
- '{"event":"UPDATE","id":"<kayıt-no>","text":"<en kapsamlı güncel hali>"}\n' +
331
- '3. DELETE: fact mevcut kayıtla ÇELİŞİYOR ve kayıt artık yanlışsa → {"event":"DELETE","id":"<kayıt-no>"}\n' +
332
- '4. NONE: fact zaten mevcut kayıtta var (anlamca aynı) → {"event":"NONE"}\n' +
333
- 'Sadece kalıcı değeri olan bilgiler; geçici detay için NONE yerine ADD yapma — ' +
334
- 'gereksizse {"event":"DROP"} dön. Çıktı: SADECE JSON {"events":[...]} — fact sırasıyla.';
335
- const candList = candidates
336
- .map((c, i) => `{"id":"${i}","text":${JSON.stringify(c.item.text)}}`)
337
- .join('\n');
338
- const factList = facts.map((f, i) => `${i + 1}. ${f}`).join('\n');
339
- const user =
340
- 'MEVCUT KAYITLAR:\n' + (candList || '(boş)') + '\n\n' +
341
- 'YENİ BİLGİLER:\n' + factList + '\n\n' +
342
- 'Her yeni bilgi için sırayla karar ver. SADECE JSON dön: {"events":[{"event":"ADD","text":"..."},{"event":"UPDATE","id":"3","text":"..."},{"event":"DELETE","id":"2"},{"event":"NONE"},{"event":"DROP"}]}';
343
- return { sys, user };
344
- }
345
-
346
- function parseEvents(raw, candidateCount, factCount) {
347
- try {
348
- const parsed = JSON.parse(String(raw || '').replace(/```(?:json)?|```/g, '').trim());
349
- const events = Array.isArray(parsed.events) ? parsed.events : [];
350
- /* fact başına 1 karar kuralı; ama DELETE+ADD "değiştirme" ikilisi meşru — pay bırak */
351
- return events.slice(0, factCount + 2).filter((e) => e && typeof e === 'object');
352
- } catch {
353
- return null; // parse edilemedi → fast-path fallback
354
- }
355
- }
356
-
357
- /* idx ("3") → gerçek item id (anti-hallucination: harici eşleme) */
358
- function resolveCandidate(candidates, idStr) {
359
- const n = Number(String(idStr).replace(/[^0-9]/g, ''));
360
- if (!Number.isInteger(n) || n < 0 || n >= candidates.length) return null;
361
- return candidates[n];
362
- }
363
-
364
- /* ---------- public API ---------- */
365
-
366
- /** Kayıt ekleme (mem0 add). llm verilirse klasik konsolidasyon, yoksa hash+cos dedup. */
367
- async function add(scope, texts, { llm } = {}) {
368
- const list = (Array.isArray(texts) ? texts : [texts])
369
- .map((t) => normText(t))
370
- .filter(Boolean)
371
- .slice(0, 8);
372
- if (!list.length) return { ok: false, error: 'empty' };
373
- const store = loadStore(scope);
374
- const newVecs = await embed(list);
375
-
376
- const findCandidates = () => {
377
- /* tüm yeni fact'lerin adaylarının birleşimi (mem0: per-fact retrieval → tek karar çağrısı) */
378
- const best = new Map(); // item.id → { item, score }
379
- for (const t of list) {
380
- const tokens = tokenize(t);
381
- for (let i = 0; i < list.length; i++) {
382
- const qvec = newVecs[i];
383
- for (const item of store.items) {
384
- const ivec = b64ToVec(item.vec);
385
- const sem = qvec && ivec ? cos(qvec, ivec) : 0;
386
- const kw = scoreEntry(item.text, tokens);
387
- const score = Math.max(sem, kw);
388
- if (score < CAND_THRESHOLD) continue;
389
- const prev = best.get(item.id);
390
- if (!prev || prev.score < score) best.set(item.id, { item, score });
391
- }
392
- }
393
- }
394
- return [...best.values()].sort((a, b) => b.score - a.score).slice(0, CAND_K * list.length);
395
- };
396
-
397
- const applyAdd = (text, vec) => {
398
- const now = new Date().toISOString();
399
- store.items.push({
400
- id: crypto.randomUUID(),
401
- text,
402
- hash: md5(text),
403
- vec: vec ? vecToB64(vec) : null,
404
- created_at: now,
405
- updated_at: now,
406
- });
407
- audit(scope, { event: 'ADD', text });
408
- return { event: 'ADD', text };
409
- };
410
- const applyUpdate = (item, text, vec) => {
411
- const old = item.text;
412
- item.text = text;
413
- item.hash = md5(text);
414
- if (vec) item.vec = vecToB64(vec);
415
- else item.vec = null;
416
- item.updated_at = new Date().toISOString();
417
- audit(scope, { event: 'UPDATE', id: item.id, old_text: old, text });
418
- return { event: 'UPDATE', id: item.id, text };
419
- };
420
- const applyDelete = (item) => {
421
- store.items = store.items.filter((it) => it.id !== item.id);
422
- audit(scope, { event: 'DELETE', id: item.id, old_text: item.text });
423
- return { event: 'DELETE', id: item.id };
424
- };
425
-
426
- const applied = [];
427
- let usedLlm = false;
428
-
429
- if (typeof llm === 'function') {
430
- try {
431
- const candidates = findCandidates();
432
- const { sys, user } = buildUpdatePrompt(list, candidates);
433
- const raw = await llm(sys, user);
434
- const events = parseEvents(raw, candidates.length, list.length);
435
- if (events) {
436
- usedLlm = true;
437
- for (const ev of events) {
438
- const evName = String(ev.event || '').toUpperCase();
439
- const cand = ev.id != null ? resolveCandidate(candidates, ev.id) : null;
440
- if (evName === 'ADD' && ev.text) {
441
- const t = normText(ev.text);
442
- if (store.items.some((it) => it.hash === md5(t))) continue;
443
- applied.push(applyAdd(t, null)); /* vektör backfill'de */
444
- } else if (evName === 'UPDATE' && cand && ev.text) {
445
- applied.push(applyUpdate(cand.item, normText(ev.text), null));
446
- } else if (evName === 'DELETE' && cand) {
447
- applied.push(applyDelete(cand.item));
448
- } else if (evName === 'NONE' || evName === 'DROP') {
449
- applied.push({ event: evName === 'DROP' ? 'DROP' : 'NONE' });
450
- }
451
- }
452
- }
453
- } catch {}
454
- }
455
-
456
- if (!usedLlm) {
457
- /* fast-path: hash dup → skip; SEMANTIC_SKIP üstü cosine → skip; yoksa ADD */
458
- for (let i = 0; i < list.length; i++) {
459
- const t = list[i];
460
- const vec = newVecs[i];
461
- if (store.items.some((it) => it.hash === md5(t))) {
462
- applied.push({ event: 'NONE', text: t });
463
- continue;
464
- }
465
- let twin = false;
466
- if (vec) {
467
- for (const it of store.items) {
468
- const ivec = b64ToVec(it.vec);
469
- if (ivec && cos(vec, ivec) >= SEMANTIC_SKIP) { twin = true; break; }
470
- }
471
- }
472
- if (twin) {
473
- applied.push({ event: 'NONE', text: t });
474
- continue;
475
- }
476
- applied.push(applyAdd(t, vec));
477
- }
478
- }
479
-
480
- /* cap aşımı: en eskilerden düş */
481
- if (store.items.length > CAP) {
482
- const overflow = store.items.length - CAP;
483
- const dropped = store.items.splice(0, overflow);
484
- for (const d of dropped) audit(scope, { event: 'CAP_DROP', id: d.id, old_text: d.text });
485
- }
486
-
487
- saveStore(scope, store);
488
- scheduleBackfill(scope);
489
- return {
490
- ok: true,
491
- llm: usedLlm,
492
- added: applied.filter((e) => e.event === 'ADD').length,
493
- updated: applied.filter((e) => e.event === 'UPDATE').length,
494
- deleted: applied.filter((e) => e.event === 'DELETE').length,
495
- skipped: applied.filter((e) => e.event === 'NONE' || e.event === 'DROP').length,
496
- events: applied,
497
- };
498
- }
499
-
500
- /** Hibrit arama: 0.65*cosine + 0.35*keyword; vektör yoksa pure keyword (degradasyon). */
501
- async function search(scope, query, { limit = 8, threshold = 0.05 } = {}) {
502
- const q = String(query || '').trim();
503
- const store = loadStore(scope);
504
- if (!store.items.length || !q) return [];
505
- const qTokens = tokenize(q);
506
- const qvec = (await embed([q]))[0];
507
- const rows = store.items.map((item) => {
508
- const ivec = b64ToVec(item.vec);
509
- const sem = qvec && ivec ? Math.max(0, cos(qvec, ivec)) : 0;
510
- const kw = scoreEntry(item.text, qTokens);
511
- const score = qvec && ivec
512
- ? SEARCH_SEM_W * sem + SEARCH_KW_W * kw
513
- : kw;
514
- return { id: item.id, text: item.text, score: Number(score.toFixed(3)), updated_at: item.updated_at };
515
- });
516
- return rows
517
- .filter((r) => r.score > threshold)
518
- .sort((a, b) => b.score - a.score)
519
- .slice(0, limit);
520
- }
521
-
522
- /** Prompt için ilgili kayıtlar ('- ' satırları; memory.relevantFor uyumlu çıktı). */
523
- async function relevant(scope, query, { maxRelevant = 6, maxRecent = 4, charCap = 2400 } = {}) {
524
- const store = loadStore(scope);
525
- if (!store.items.length) return '';
526
- const picked = new Set();
527
- try {
528
- const hits = await search(scope, query, { limit: maxRelevant, threshold: 0.05 });
529
- for (const h of hits) picked.add(h.id);
530
- } catch {}
531
- /* en yeniler de daima girer (memory.relevantFor davranışı) */
532
- for (let i = store.items.length - 1; i >= 0 && picked.size < maxRelevant + maxRecent; i--) {
533
- picked.add(store.items[i].id);
534
- }
535
- /* çıktı: insertion sırası korunur (ayna dosyasıyla tutarlı) */
536
- let out = '';
537
- for (const it of store.items) {
538
- if (!picked.has(it.id)) continue;
539
- const line = '- ' + it.text;
540
- if (out.length + line.length > charCap) break;
541
- out += (out ? '\n' : '') + line;
542
- }
543
- return out;
544
- }
545
-
546
- function recentTexts(scope, n = 30) {
547
- try {
548
- return loadStore(scope).items.slice(-n).map((it) => it.text);
549
- } catch {
550
- return [];
551
- }
552
- }
553
-
554
- function stats(scope) {
555
- const store = loadStore(scope);
556
- return {
557
- scope: String(scope),
558
- items: store.items.length,
559
- withVec: store.items.filter((it) => it.vec).length,
560
- cap: CAP,
561
- };
562
- }
563
-
564
- /** mem0 FACT_RETRIEVAL (TR uyarlaması): sohbetten kalıcı fact'ler çıkarır. */
565
- async function extractFacts(llm, transcript, existing, { max = 3 } = {}) {
566
- if (typeof llm !== 'function') return [];
567
- const sys =
568
- 'Aşağıdaki konuşma parçasından UZUN VADELİ hatırlanmaya değer bilgileri çıkar ' +
569
- '(kullanıcının kalıcı tercihleri, projeleri/işleri, isimler, rutinler, düzeltmeler).\n' +
570
- 'KURALLAR:\n' +
571
- `- Sadece haftalar sonra bile işe yarayacak şeyler; geçici detay asla\n` +
572
- `- En fazla ${max} madde, her biri max 120 karakter, "kullanıcı ..." diye başla\n` +
573
- '- ZATEN KAYITLI olanlarla anlamca aynıysa tekrar ekleme\n' +
574
- '- Değerli bilgi yoksa boş dizi\n' +
575
- 'SADECE JSON dön: {"memories": ["...", "..."]} veya {"memories": []}';
576
- const user =
577
- 'ZATEN KAYITLI olanlar (tekrar ekleme):\n' + (existing || '(boş)') + '\n\n' +
578
- '# SOHBET\n' + transcript;
579
- try {
580
- const raw = await llm(sys, user);
581
- const parsed = JSON.parse(String(raw || '').replace(/```(?:json)?|```/g, '').trim());
582
- const arr = parsed && Array.isArray(parsed.memories) ? parsed.memories : [];
583
- return arr.map((m) => normText(m)).filter(Boolean).slice(0, max);
584
- } catch {
585
- return [];
586
- }
587
- }
588
-
589
- module.exports = {
590
- add,
591
- search,
592
- relevant,
593
- recentTexts,
594
- extractFacts,
595
- syncMirror,
596
- reindexFromLines,
597
- history,
598
- stats,
599
- setEmbedder,
600
- mirrorLines,
601
- CAP,
602
- DIMS,
603
- SEMANTIC_SKIP,
604
- CAND_THRESHOLD,
605
- };
1
+ 'use strict';
2
+
3
+ /* mem0-native: mem0 mimarisinin (github.com/mem0ai/mem0) Beast'e uyarlanmış
4
+ Node çekirdeği. Python/mem0-ts BAĞIMLILIĞI YOK:
5
+ - Vektör: @xenova/transformers (whisper için zaten var) → all-MiniLM-L6-v2 (384d)
6
+ - Store: JSON dosya per scope → bot başına AYRI dosya = tam izolasyon
7
+ - Konsolidasyon: mem0 klasik update prompt'u → ADD / UPDATE / DELETE / NONE
8
+ - Audit: JSONL geçmiş (mem0 history.db karşılığı)
9
+ - Hibrit arama: 0.65*semantic(cosine) + 0.35*keyword (mem0 semantic+BM25 felsefesi)
10
+ Her fonksiyon degradasyonlu: embedding yoksa keyword, LLM yoksa hash+cos dedup.
11
+ Scope: 'main' | 'bot:<botId>' — botlar arasında ASLA çapraz erişim yok. */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const crypto = require('crypto');
17
+
18
+ const CAP = 400; // scope başına maksimum kayıt (memory.js cap ile aynı)
19
+ const DIMS = 384; // all-MiniLM-L6-v2
20
+ const SEMANTIC_SKIP = 0.93; // add fast-path: bu cosine üstü aynı bilgi sayılır
21
+ const CAND_THRESHOLD = 0.3; // konsolidasyon adayı için min cosine
22
+ const CAND_K = 6; // fact başına çekilecek benzer aday
23
+ const SEARCH_SEM_W = 0.65; // hibrit skor: semantik ağırlık
24
+ const SEARCH_KW_W = 0.35; // hibrit skor: keyword ağırlık
25
+ const LOAD_TIMEOUT_MS = 10000; // embed modeli ilk yüklemeye bu süreyi bekler, sonra vektörsüz devam
26
+
27
+ /* ---------- yollar ---------- */
28
+
29
+ function beastRoot() {
30
+ if (process.env.BEAST_DATA) return process.env.BEAST_DATA;
31
+ return process.env.APPDATA
32
+ ? path.join(process.env.APPDATA, 'beast')
33
+ : path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
34
+ }
35
+
36
+ function mem0Dir() {
37
+ const d = path.join(beastRoot(), 'memories', 'mem0');
38
+ try { fs.mkdirSync(d, { recursive: true }); } catch {}
39
+ return d;
40
+ }
41
+
42
+ /* scope → güvenli dosya adı ('bot:ahmet-42' → 'bot-ahmet-42') */
43
+ function fileSafe(scope) {
44
+ return String(scope || 'main').replace(/[^a-zA-Z0-9._-]+/g, '-');
45
+ }
46
+
47
+ function storePath(scope) {
48
+ return path.join(mem0Dir(), fileSafe(scope) + '.json');
49
+ }
50
+
51
+ function logPath(scope) {
52
+ return path.join(mem0Dir(), fileSafe(scope) + '.log');
53
+ }
54
+
55
+ /* ---------- metin skorlama (memory.js ile aynı klasikler — öz-bağımsız) ---------- */
56
+
57
+ const TR_FOLD = { ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u' };
58
+
59
+ function fold(s) {
60
+ return String(s || '')
61
+ .toLowerCase()
62
+ .replace(/[çğıöşüâîû]/g, (ch) => TR_FOLD[ch] || ch);
63
+ }
64
+
65
+ function tokenize(q) {
66
+ return (fold(q).match(/[a-z0-9_+#.]+/g) || []).filter((w) => w.length >= 3);
67
+ }
68
+
69
+ function normText(t) {
70
+ return String(t || '').replace(/\s+/g, ' ').trim().slice(0, 500);
71
+ }
72
+
73
+ function md5(s) {
74
+ return crypto.createHash('md5').update(String(s || ''), 'utf8').digest('hex');
75
+ }
76
+
77
+ function scoreEntry(entryText, qTokens) {
78
+ if (!qTokens.length) return 0;
79
+ const hay = fold(entryText);
80
+ let hits = 0;
81
+ for (const t of new Set(qTokens)) if (hay.includes(t)) hits++;
82
+ return hits / Math.sqrt(qTokens.length);
83
+ }
84
+
85
+ /* ---------- vektör sağlayıcı ---------- */
86
+
87
+ let _embedFn = null; // test enjeksiyonu
88
+ let _pipe = null;
89
+ let _pipeLoading = null;
90
+ let _pipeBroken = false;
91
+
92
+ /** Test/ar-ge: harici embedder tak (async (texts) => Float32Array[] | null) */
93
+ function setEmbedder(fn) {
94
+ _embedFn = typeof fn === 'function' ? fn : null;
95
+ }
96
+
97
+ function loadPipeline() {
98
+ if (_pipe) return Promise.resolve(_pipe);
99
+ if (_pipeLoading) return _pipeLoading;
100
+ if (_pipeBroken) return Promise.resolve(null);
101
+ _pipeLoading = (async () => {
102
+ try {
103
+ const { pipeline, env } = require('@xenova/transformers');
104
+ const modelsDir = process.env.BEAST_MODELS_DIR || path.join(beastRoot(), 'models');
105
+ fs.mkdirSync(modelsDir, { recursive: true });
106
+ env.cacheDir = modelsDir;
107
+ env.allowLocalModels = false;
108
+ const p = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { quantized: true });
109
+ _pipe = async (texts) => {
110
+ const out = [];
111
+ for (const t of texts) {
112
+ const tensor = await p(t, { pooling: 'mean', normalize: true });
113
+ const arr = new Float32Array(DIMS);
114
+ const data = tensor.data;
115
+ for (let i = 0; i < DIMS && i < data.length; i++) arr[i] = data[i];
116
+ out.push(arr);
117
+ }
118
+ return out;
119
+ };
120
+ return _pipe;
121
+ } catch {
122
+ _pipeBroken = true; // bir daha deneme — keyword-only moda düş
123
+ return null;
124
+ } finally {
125
+ _pipeLoading = null;
126
+ }
127
+ })();
128
+ return _pipeLoading;
129
+ }
130
+
131
+ /* embedder hazırsa döner; ilk yükleme LOAD_TIMEOUT_MS'ı geçerse null (vektörsüz devam) */
132
+ async function getEmbedder() {
133
+ if (_embedFn) return _embedFn;
134
+ const p = loadPipeline();
135
+ let timer;
136
+ const timeout = new Promise((res) => { timer = setTimeout(() => res(null), LOAD_TIMEOUT_MS); });
137
+ const loaded = await Promise.race([p, timeout]).catch(() => null);
138
+ clearTimeout(timer);
139
+ return loaded || null;
140
+ }
141
+
142
+ async function embed(texts) {
143
+ const fn = await getEmbedder();
144
+ if (!fn) return texts.map(() => null);
145
+ try {
146
+ const vecs = await fn(texts);
147
+ if (!Array.isArray(vecs) || vecs.length !== texts.length) return texts.map(() => null);
148
+ return vecs.map((v) => (v instanceof Float32Array && v.length === DIMS ? v : null));
149
+ } catch {
150
+ return texts.map(() => null);
151
+ }
152
+ }
153
+
154
+ function cos(a, b) {
155
+ if (!a || !b) return 0;
156
+ let dot = 0;
157
+ for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
158
+ return dot; // normalize vektörler → dot = cosine
159
+ }
160
+
161
+ function vecToB64(v) {
162
+ if (!v) return null;
163
+ return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString('base64');
164
+ }
165
+
166
+ function b64ToVec(s) {
167
+ if (!s) return null;
168
+ try {
169
+ const buf = Buffer.from(String(s), 'base64');
170
+ if (buf.length !== DIMS * 4) return null;
171
+ return new Float32Array(buf.buffer, buf.byteOffset, DIMS);
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ /* ---------- store ---------- */
178
+
179
+ /* mevcut ayna dosyasından satırlar (MEMORY.md) — boot import + admin düzenleme sonrası */
180
+ function mirrorLines(scope) {
181
+ try {
182
+ let p = null;
183
+ if (scope === 'main') p = path.join(beastRoot(), 'memories', 'MEMORY.md');
184
+ else if (scope.startsWith('bot:')) {
185
+ const bots = require('./bots');
186
+ p = bots.memPath(scope.slice(4));
187
+ }
188
+ if (!p) return [];
189
+ return fs
190
+ .readFileSync(p, 'utf8')
191
+ .split('\n')
192
+ .map((l) => l.replace(/^[-*]\s*/, '').trim())
193
+ .filter(Boolean);
194
+ } catch {
195
+ return [];
196
+ }
197
+ }
198
+
199
+ function loadStore(scope) {
200
+ try {
201
+ const raw = fs.readFileSync(storePath(scope), 'utf8');
202
+ const data = JSON.parse(raw);
203
+ if (data && Array.isArray(data.items)) return data;
204
+ } catch {}
205
+ /* store dosyası yok → mevcut MEMORY.md satırlarından import (vektörsüz, backfill sonra) */
206
+ const items = mirrorLines(scope).map((text) => ({
207
+ id: crypto.randomUUID(),
208
+ text: normText(text),
209
+ hash: md5(normText(text)),
210
+ vec: null,
211
+ created_at: new Date().toISOString(),
212
+ updated_at: new Date().toISOString(),
213
+ }));
214
+ return { version: 1, items };
215
+ }
216
+
217
+ function saveStore(scope, store) {
218
+ try {
219
+ fs.writeFileSync(storePath(scope), JSON.stringify(store), 'utf8');
220
+ return true;
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+
226
+ /* audit günlüğü (mem0 history.db karşılığı — JSONL) */
227
+ function audit(scope, entry) {
228
+ try {
229
+ fs.appendFileSync(logPath(scope), JSON.stringify({ at: new Date().toISOString(), ...entry }) + '\n');
230
+ } catch {}
231
+ }
232
+
233
+ function history(scope, limit = 50) {
234
+ try {
235
+ const rows = fs
236
+ .readFileSync(logPath(scope), 'utf8')
237
+ .split('\n')
238
+ .filter(Boolean)
239
+ .map((l) => {
240
+ try { return JSON.parse(l); } catch { return null; }
241
+ })
242
+ .filter(Boolean);
243
+ return rows.slice(-limit).reverse();
244
+ } catch {
245
+ return [];
246
+ }
247
+ }
248
+
249
+ /* MEMORY.md aynasını store'dan yeniden yaz (store = doğruluk kaynağı) */
250
+ function syncMirror(scope) {
251
+ try {
252
+ const store = loadStore(scope);
253
+ const body = store.items.map((it) => '- ' + it.text).join('\n') + (store.items.length ? '\n' : '');
254
+ let p = null;
255
+ if (scope === 'main') {
256
+ const memory = require('./memory');
257
+ p = path.join(memory.memDir(), 'MEMORY.md');
258
+ } else if (scope.startsWith('bot:')) {
259
+ const bots = require('./bots');
260
+ p = bots.memPath(scope.slice(4));
261
+ }
262
+ if (!p) return false;
263
+ fs.mkdirSync(path.dirname(p), { recursive: true });
264
+ fs.writeFileSync(p, body, 'utf8');
265
+ return true;
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+
271
+ /* admin/UI MEMORY.md'yi elle değiştirdiyse store'u satırlardan yeniden kur */
272
+ function reindexFromLines(scope, lines) {
273
+ const items = (Array.isArray(lines) ? lines : [])
274
+ .map((t) => normText(t))
275
+ .filter(Boolean)
276
+ .map((text) => ({
277
+ id: crypto.randomUUID(),
278
+ text,
279
+ hash: md5(text),
280
+ vec: null,
281
+ created_at: new Date().toISOString(),
282
+ updated_at: new Date().toISOString(),
283
+ }));
284
+ const store = { version: 1, items };
285
+ saveStore(scope, store);
286
+ audit(scope, { event: 'IMPORT', count: items.length });
287
+ scheduleBackfill(scope);
288
+ return { ok: true, count: items.length };
289
+ }
290
+
291
+ /* ---------- arka plan vektör doldurma ---------- */
292
+
293
+ const _backfillRunning = new Set();
294
+
295
+ function scheduleBackfill(scope) {
296
+ if (_backfillRunning.has(scope)) return;
297
+ _backfillRunning.add(scope);
298
+ setTimeout(() => {
299
+ _backfill(scope)
300
+ .catch(() => {})
301
+ .finally(() => _backfillRunning.delete(scope));
302
+ }, 1200).unref();
303
+ }
304
+
305
+ async function _backfill(scope) {
306
+ const fn = await getEmbedder();
307
+ if (!fn) return;
308
+ const store = loadStore(scope);
309
+ const todo = store.items.filter((it) => !it.vec).slice(0, 64);
310
+ if (!todo.length) return;
311
+ const texts = todo.map((it) => it.text);
312
+ const vecs = await embed(texts);
313
+ todo.forEach((it, i) => {
314
+ if (vecs[i]) it.vec = vecToB64(vecs[i]);
315
+ });
316
+ saveStore(scope, store);
317
+ if (store.items.some((it) => !it.vec)) scheduleBackfill(scope);
318
+ }
319
+
320
+ /* ---------- mem0 klasik konsolidasyon ---------- */
321
+
322
+ /* mem0 update prompt'u: yeni fact + benzer mevcut kayıtlar → tek LLM karar çağrısı.
323
+ ID'ler indeks ("0","1",...) — LLM'in UUID halüsinasyonuna karşı (mem0 anti-hallucination). */
324
+ function buildUpdatePrompt(facts, candidates) {
325
+ const sys =
326
+ 'Sen bir uzun vadeli hafıza yöneticisisin. Yeni bilgi (fact) ile mevcut kayıtları karşılaştırıp ' +
327
+ 'her fact için TEK karar ver. KURALLAR:\n' +
328
+ '1. ADD: hafızada olmayan yeni bilgi → {"event":"ADD","text":"<fact>"}\n' +
329
+ '2. UPDATE: mevcut kayıtla AYNI konu ama bilgi değişmiş/güncellenmiş → ' +
330
+ '{"event":"UPDATE","id":"<kayıt-no>","text":"<en kapsamlı güncel hali>"}\n' +
331
+ '3. DELETE: fact mevcut kayıtla ÇELİŞİYOR ve kayıt artık yanlışsa → {"event":"DELETE","id":"<kayıt-no>"}\n' +
332
+ '4. NONE: fact zaten mevcut kayıtta var (anlamca aynı) → {"event":"NONE"}\n' +
333
+ 'Sadece kalıcı değeri olan bilgiler; geçici detay için NONE yerine ADD yapma — ' +
334
+ 'gereksizse {"event":"DROP"} dön. Çıktı: SADECE JSON {"events":[...]} — fact sırasıyla.';
335
+ const candList = candidates
336
+ .map((c, i) => `{"id":"${i}","text":${JSON.stringify(c.item.text)}}`)
337
+ .join('\n');
338
+ const factList = facts.map((f, i) => `${i + 1}. ${f}`).join('\n');
339
+ const user =
340
+ 'MEVCUT KAYITLAR:\n' + (candList || '(boş)') + '\n\n' +
341
+ 'YENİ BİLGİLER:\n' + factList + '\n\n' +
342
+ 'Her yeni bilgi için sırayla karar ver. SADECE JSON dön: {"events":[{"event":"ADD","text":"..."},{"event":"UPDATE","id":"3","text":"..."},{"event":"DELETE","id":"2"},{"event":"NONE"},{"event":"DROP"}]}';
343
+ return { sys, user };
344
+ }
345
+
346
+ function parseEvents(raw, candidateCount, factCount) {
347
+ try {
348
+ const parsed = JSON.parse(String(raw || '').replace(/```(?:json)?|```/g, '').trim());
349
+ const events = Array.isArray(parsed.events) ? parsed.events : [];
350
+ /* fact başına 1 karar kuralı; ama DELETE+ADD "değiştirme" ikilisi meşru — pay bırak */
351
+ return events.slice(0, factCount + 2).filter((e) => e && typeof e === 'object');
352
+ } catch {
353
+ return null; // parse edilemedi → fast-path fallback
354
+ }
355
+ }
356
+
357
+ /* idx ("3") → gerçek item id (anti-hallucination: harici eşleme) */
358
+ function resolveCandidate(candidates, idStr) {
359
+ const n = Number(String(idStr).replace(/[^0-9]/g, ''));
360
+ if (!Number.isInteger(n) || n < 0 || n >= candidates.length) return null;
361
+ return candidates[n];
362
+ }
363
+
364
+ /* ---------- public API ---------- */
365
+
366
+ /** Kayıt ekleme (mem0 add). llm verilirse klasik konsolidasyon, yoksa hash+cos dedup. */
367
+ async function add(scope, texts, { llm } = {}) {
368
+ const list = (Array.isArray(texts) ? texts : [texts])
369
+ .map((t) => normText(t))
370
+ .filter(Boolean)
371
+ .slice(0, 8);
372
+ if (!list.length) return { ok: false, error: 'empty' };
373
+ const store = loadStore(scope);
374
+ const newVecs = await embed(list);
375
+
376
+ const findCandidates = () => {
377
+ /* tüm yeni fact'lerin adaylarının birleşimi (mem0: per-fact retrieval → tek karar çağrısı) */
378
+ const best = new Map(); // item.id → { item, score }
379
+ for (const t of list) {
380
+ const tokens = tokenize(t);
381
+ for (let i = 0; i < list.length; i++) {
382
+ const qvec = newVecs[i];
383
+ for (const item of store.items) {
384
+ const ivec = b64ToVec(item.vec);
385
+ const sem = qvec && ivec ? cos(qvec, ivec) : 0;
386
+ const kw = scoreEntry(item.text, tokens);
387
+ const score = Math.max(sem, kw);
388
+ if (score < CAND_THRESHOLD) continue;
389
+ const prev = best.get(item.id);
390
+ if (!prev || prev.score < score) best.set(item.id, { item, score });
391
+ }
392
+ }
393
+ }
394
+ return [...best.values()].sort((a, b) => b.score - a.score).slice(0, CAND_K * list.length);
395
+ };
396
+
397
+ const applyAdd = (text, vec) => {
398
+ const now = new Date().toISOString();
399
+ store.items.push({
400
+ id: crypto.randomUUID(),
401
+ text,
402
+ hash: md5(text),
403
+ vec: vec ? vecToB64(vec) : null,
404
+ created_at: now,
405
+ updated_at: now,
406
+ });
407
+ audit(scope, { event: 'ADD', text });
408
+ return { event: 'ADD', text };
409
+ };
410
+ const applyUpdate = (item, text, vec) => {
411
+ const old = item.text;
412
+ item.text = text;
413
+ item.hash = md5(text);
414
+ if (vec) item.vec = vecToB64(vec);
415
+ else item.vec = null;
416
+ item.updated_at = new Date().toISOString();
417
+ audit(scope, { event: 'UPDATE', id: item.id, old_text: old, text });
418
+ return { event: 'UPDATE', id: item.id, text };
419
+ };
420
+ const applyDelete = (item) => {
421
+ store.items = store.items.filter((it) => it.id !== item.id);
422
+ audit(scope, { event: 'DELETE', id: item.id, old_text: item.text });
423
+ return { event: 'DELETE', id: item.id };
424
+ };
425
+
426
+ const applied = [];
427
+ let usedLlm = false;
428
+
429
+ if (typeof llm === 'function') {
430
+ try {
431
+ const candidates = findCandidates();
432
+ const { sys, user } = buildUpdatePrompt(list, candidates);
433
+ const raw = await llm(sys, user);
434
+ const events = parseEvents(raw, candidates.length, list.length);
435
+ if (events) {
436
+ usedLlm = true;
437
+ for (const ev of events) {
438
+ const evName = String(ev.event || '').toUpperCase();
439
+ const cand = ev.id != null ? resolveCandidate(candidates, ev.id) : null;
440
+ if (evName === 'ADD' && ev.text) {
441
+ const t = normText(ev.text);
442
+ if (store.items.some((it) => it.hash === md5(t))) continue;
443
+ applied.push(applyAdd(t, null)); /* vektör backfill'de */
444
+ } else if (evName === 'UPDATE' && cand && ev.text) {
445
+ applied.push(applyUpdate(cand.item, normText(ev.text), null));
446
+ } else if (evName === 'DELETE' && cand) {
447
+ applied.push(applyDelete(cand.item));
448
+ } else if (evName === 'NONE' || evName === 'DROP') {
449
+ applied.push({ event: evName === 'DROP' ? 'DROP' : 'NONE' });
450
+ }
451
+ }
452
+ }
453
+ } catch {}
454
+ }
455
+
456
+ if (!usedLlm) {
457
+ /* fast-path: hash dup → skip; SEMANTIC_SKIP üstü cosine → skip; yoksa ADD */
458
+ for (let i = 0; i < list.length; i++) {
459
+ const t = list[i];
460
+ const vec = newVecs[i];
461
+ if (store.items.some((it) => it.hash === md5(t))) {
462
+ applied.push({ event: 'NONE', text: t });
463
+ continue;
464
+ }
465
+ let twin = false;
466
+ if (vec) {
467
+ for (const it of store.items) {
468
+ const ivec = b64ToVec(it.vec);
469
+ if (ivec && cos(vec, ivec) >= SEMANTIC_SKIP) { twin = true; break; }
470
+ }
471
+ }
472
+ if (twin) {
473
+ applied.push({ event: 'NONE', text: t });
474
+ continue;
475
+ }
476
+ applied.push(applyAdd(t, vec));
477
+ }
478
+ }
479
+
480
+ /* cap aşımı: en eskilerden düş */
481
+ if (store.items.length > CAP) {
482
+ const overflow = store.items.length - CAP;
483
+ const dropped = store.items.splice(0, overflow);
484
+ for (const d of dropped) audit(scope, { event: 'CAP_DROP', id: d.id, old_text: d.text });
485
+ }
486
+
487
+ saveStore(scope, store);
488
+ scheduleBackfill(scope);
489
+ return {
490
+ ok: true,
491
+ llm: usedLlm,
492
+ added: applied.filter((e) => e.event === 'ADD').length,
493
+ updated: applied.filter((e) => e.event === 'UPDATE').length,
494
+ deleted: applied.filter((e) => e.event === 'DELETE').length,
495
+ skipped: applied.filter((e) => e.event === 'NONE' || e.event === 'DROP').length,
496
+ events: applied,
497
+ };
498
+ }
499
+
500
+ /** Hibrit arama: 0.65*cosine + 0.35*keyword; vektör yoksa pure keyword (degradasyon). */
501
+ async function search(scope, query, { limit = 8, threshold = 0.05 } = {}) {
502
+ const q = String(query || '').trim();
503
+ const store = loadStore(scope);
504
+ if (!store.items.length || !q) return [];
505
+ const qTokens = tokenize(q);
506
+ const qvec = (await embed([q]))[0];
507
+ const rows = store.items.map((item) => {
508
+ const ivec = b64ToVec(item.vec);
509
+ const sem = qvec && ivec ? Math.max(0, cos(qvec, ivec)) : 0;
510
+ const kw = scoreEntry(item.text, qTokens);
511
+ const score = qvec && ivec
512
+ ? SEARCH_SEM_W * sem + SEARCH_KW_W * kw
513
+ : kw;
514
+ return { id: item.id, text: item.text, score: Number(score.toFixed(3)), updated_at: item.updated_at };
515
+ });
516
+ return rows
517
+ .filter((r) => r.score > threshold)
518
+ .sort((a, b) => b.score - a.score)
519
+ .slice(0, limit);
520
+ }
521
+
522
+ /** Prompt için ilgili kayıtlar ('- ' satırları; memory.relevantFor uyumlu çıktı). */
523
+ async function relevant(scope, query, { maxRelevant = 6, maxRecent = 4, charCap = 2400 } = {}) {
524
+ const store = loadStore(scope);
525
+ if (!store.items.length) return '';
526
+ const picked = new Set();
527
+ try {
528
+ const hits = await search(scope, query, { limit: maxRelevant, threshold: 0.05 });
529
+ for (const h of hits) picked.add(h.id);
530
+ } catch {}
531
+ /* en yeniler de daima girer (memory.relevantFor davranışı) */
532
+ for (let i = store.items.length - 1; i >= 0 && picked.size < maxRelevant + maxRecent; i--) {
533
+ picked.add(store.items[i].id);
534
+ }
535
+ /* çıktı: insertion sırası korunur (ayna dosyasıyla tutarlı) */
536
+ let out = '';
537
+ for (const it of store.items) {
538
+ if (!picked.has(it.id)) continue;
539
+ const line = '- ' + it.text;
540
+ if (out.length + line.length > charCap) break;
541
+ out += (out ? '\n' : '') + line;
542
+ }
543
+ return out;
544
+ }
545
+
546
+ function recentTexts(scope, n = 30) {
547
+ try {
548
+ return loadStore(scope).items.slice(-n).map((it) => it.text);
549
+ } catch {
550
+ return [];
551
+ }
552
+ }
553
+
554
+ function stats(scope) {
555
+ const store = loadStore(scope);
556
+ return {
557
+ scope: String(scope),
558
+ items: store.items.length,
559
+ withVec: store.items.filter((it) => it.vec).length,
560
+ cap: CAP,
561
+ };
562
+ }
563
+
564
+ /** mem0 FACT_RETRIEVAL (TR uyarlaması): sohbetten kalıcı fact'ler çıkarır. */
565
+ async function extractFacts(llm, transcript, existing, { max = 3 } = {}) {
566
+ if (typeof llm !== 'function') return [];
567
+ const sys =
568
+ 'Aşağıdaki konuşma parçasından UZUN VADELİ hatırlanmaya değer bilgileri çıkar ' +
569
+ '(kullanıcının kalıcı tercihleri, projeleri/işleri, isimler, rutinler, düzeltmeler).\n' +
570
+ 'KURALLAR:\n' +
571
+ `- Sadece haftalar sonra bile işe yarayacak şeyler; geçici detay asla\n` +
572
+ `- En fazla ${max} madde, her biri max 120 karakter, "kullanıcı ..." diye başla\n` +
573
+ '- ZATEN KAYITLI olanlarla anlamca aynıysa tekrar ekleme\n' +
574
+ '- Değerli bilgi yoksa boş dizi\n' +
575
+ 'SADECE JSON dön: {"memories": ["...", "..."]} veya {"memories": []}';
576
+ const user =
577
+ 'ZATEN KAYITLI olanlar (tekrar ekleme):\n' + (existing || '(boş)') + '\n\n' +
578
+ '# SOHBET\n' + transcript;
579
+ try {
580
+ const raw = await llm(sys, user);
581
+ const parsed = JSON.parse(String(raw || '').replace(/```(?:json)?|```/g, '').trim());
582
+ const arr = parsed && Array.isArray(parsed.memories) ? parsed.memories : [];
583
+ return arr.map((m) => normText(m)).filter(Boolean).slice(0, max);
584
+ } catch {
585
+ return [];
586
+ }
587
+ }
588
+
589
+ module.exports = {
590
+ add,
591
+ search,
592
+ relevant,
593
+ recentTexts,
594
+ extractFacts,
595
+ syncMirror,
596
+ reindexFromLines,
597
+ history,
598
+ stats,
599
+ setEmbedder,
600
+ mirrorLines,
601
+ CAP,
602
+ DIMS,
603
+ SEMANTIC_SKIP,
604
+ CAND_THRESHOLD,
605
+ };