beast-agent 2.4.1 → 2.5.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.4.1",
4
+ "version": "2.5.1",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -987,9 +987,11 @@ class Engine {
987
987
  if (session.code) this._codeIndex.set(session.code, session.id);
988
988
  }
989
989
 
990
- /* Oturum dosyasındaki son 'msg' satırını (user) güncel içerikle değiştir —
991
- yanıtsız kalmış user mesajıyla yeni mesaj birleştirildiğinde kullanılır */
992
- _rewriteLastMsg(id, content, attachments) {
990
+ /* Oturum dosyasındaki son 'msg' satırını güncel içerikle değiştir —
991
+ yanıtsız kalmış user mesajıyla yeni mesaj birleştirildiğinde (role:'user')
992
+ ya da proaktif not son asistan mesajına birleştirildiğinde (role:'assistant') */
993
+ _rewriteLastMsg(id, content, attachments, role) {
994
+ const wantRole = role === 'assistant' ? 'assistant' : 'user';
993
995
  try {
994
996
  const file = this._file(String(id));
995
997
  const lines = fs.readFileSync(file, 'utf8').split('\n');
@@ -997,8 +999,8 @@ class Engine {
997
999
  if (!lines[i].trim()) continue;
998
1000
  let r;
999
1001
  try { r = JSON.parse(lines[i]); } catch { continue; }
1000
- if (r.t === 'msg' && r.role === 'user') {
1001
- const out = { t: 'msg', role: 'user', content: String(content || '') };
1002
+ if (r.t === 'msg' && r.role === wantRole) {
1003
+ const out = { t: 'msg', role: wantRole, content: String(content || '') };
1002
1004
  if (Array.isArray(attachments) && attachments.length) out.attachments = attachments;
1003
1005
  lines[i] = JSON.stringify(out);
1004
1006
  break;
@@ -1092,6 +1094,9 @@ class Engine {
1092
1094
  session.isBotDm = true;
1093
1095
  session.dmA = String(rec.a || '');
1094
1096
  session.dmB = String(rec.b || '');
1097
+ } else if (rec.t === 'bcws') {
1098
+ /* Beast Code oturumu: hangi çalışma klasörüne bağlı */
1099
+ session.bcWs = String(rec.ws || '');
1095
1100
  } else if (rec.t === 'msg') {
1096
1101
  delete rec.t;
1097
1102
  session.messages.push(rec);
@@ -1251,6 +1256,42 @@ class Engine {
1251
1256
  return out.slice(0, 100);
1252
1257
  }
1253
1258
 
1259
+ /* Beast Code panel oturumları: bgTitle 'Beast Code' olan gizli oturumlar.
1260
+ Soldaki dosya panelinin sohbet geçmişi listesi bunları gösterir —
1261
+ ana sohbet listesinden (listSessions) TAMAMEN AYRIDIR. */
1262
+ listBcSessions(limit = 100) {
1263
+ let files = [];
1264
+ try {
1265
+ files = fs.readdirSync(this.sessionsDir).filter((f) => f.endsWith('.jsonl'));
1266
+ } catch {}
1267
+ const out = [];
1268
+ for (const f of files) {
1269
+ const id = f.replace(/\.jsonl$/, '');
1270
+ let s;
1271
+ try { s = this._load(id); } catch { continue; }
1272
+ if (s.bgTitle !== 'Beast Code') continue; // yalnız Beast Code oturumları
1273
+ let title = '';
1274
+ for (const m of s.messages) {
1275
+ if (m.role !== 'user') continue;
1276
+ const t = Array.isArray(m.content)
1277
+ ? m.content.filter((p) => p && p.type === 'text').map((p) => String(p.text || '')).join(' ')
1278
+ : String(m.content || '');
1279
+ title = t.replace(/\s+/g, ' ').trim().slice(0, 64);
1280
+ if (title) break;
1281
+ }
1282
+ out.push({
1283
+ id,
1284
+ title: title || 'Beast Code oturumu',
1285
+ updatedAt: s.updatedAt,
1286
+ createdAt: s.createdAt,
1287
+ count: s.messages.length,
1288
+ ws: s.bcWs || '',
1289
+ });
1290
+ }
1291
+ out.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
1292
+ return out.slice(0, Math.max(1, Math.min(200, Number(limit) || 100)));
1293
+ }
1294
+
1254
1295
  createSession() {
1255
1296
  const id = uid();
1256
1297
  const code = this._newCode();
@@ -2284,6 +2325,40 @@ class Engine {
2284
2325
  return true;
2285
2326
  }
2286
2327
 
2328
+ /* PROAKTİF MESAJ ENJEKSİYONU (Empati Loop): ajanın az önce KULLANICIYA
2329
+ bildirdiği metni, yeni bir tur BAŞLATMADAN geçmişe kendi asistan
2330
+ mesajı olarak işler. Model bir sonraki turda bu mesajı bağlamında
2331
+ "benim söylediğim söz" olarak görür — kullanıcı cevap verdiğinde
2332
+ sohbet aynı oturumda kopmadan devam eder. Son mesaj araçsız bir
2333
+ asistan mesajıysa metin ona BİRLEŞTİRİLİR — katı sağlayıcılarda
2334
+ art arda asistan mesajı gitmez. */
2335
+ injectAssistant(sessionId, text) {
2336
+ const s = this._load(String(sessionId));
2337
+ if (!s) return false;
2338
+ const body = String(text || '').slice(0, USER_MAX);
2339
+ if (!body.trim()) return false;
2340
+ const lastMsg = s.messages[s.messages.length - 1];
2341
+ if (
2342
+ lastMsg &&
2343
+ lastMsg.role === 'assistant' &&
2344
+ typeof lastMsg.content === 'string' &&
2345
+ !(Array.isArray(lastMsg.tool_calls) && lastMsg.tool_calls.length)
2346
+ ) {
2347
+ lastMsg.content = (lastMsg.content + '\n\n' + body).slice(0, USER_MAX);
2348
+ this._rewriteLastMsg(s.id, lastMsg.content, null, 'assistant');
2349
+ this.emit({ type: 'sessions' });
2350
+ return true;
2351
+ }
2352
+ const msg = { role: 'assistant', content: body };
2353
+ s.messages.push(msg);
2354
+ try {
2355
+ this._append(s, msg);
2356
+ } catch {}
2357
+ this.emit({ type: 'message', sessionId: s.id, message: msg });
2358
+ this.emit({ type: 'sessions' });
2359
+ return true;
2360
+ }
2361
+
2287
2362
  /* Paralel ajan GEÇMİŞİNİ topluca sil: çalışanlar/ bekleyenler iptal edilir,
2288
2363
  tüm bg oturum dosyaları + kalıcı kayıt temizlenir. Döndürür: silinen iş sayısı */
2289
2364
  clearAllBgJobs() {
@@ -3668,6 +3743,9 @@ const skills = require('./skills');
3668
3743
  clearTimeout(kill);
3669
3744
  }
3670
3745
  };
3746
+ /* Empati Loop ilgi çıkarımı: günde bir, konuşma hafızasından LLM ile
3747
+ (kelime-frekans öğrenici kaldırıldı — çöp etiket üretiyordu) */
3748
+ const perception = require('./perception');
3671
3749
  return await nightref.run({
3672
3750
  llm,
3673
3751
  memory,
@@ -3676,6 +3754,10 @@ const skills = require('./skills');
3676
3754
  sinceIso,
3677
3755
  manual,
3678
3756
  now: new Date(),
3757
+ interests: {
3758
+ prompt: () => perception.interestPrompt(),
3759
+ apply: (raw) => perception.applyInferredInterests(raw),
3760
+ },
3679
3761
  log: (msg) => log.info('gece-yansima', msg),
3680
3762
  });
3681
3763
  } finally {
@@ -354,6 +354,38 @@ async function run(deps) {
354
354
  report.notes.push('kayıt sayısı az (' + entriesNow.length + ') — sıkılaştırma atlandı');
355
355
  }
356
356
 
357
+ /* 3b) ilgi alanı çıkarımı (Empati Loop, günde bir): kelime-frekans öğrenme
358
+ YOK — model birikmiş konuşma hafızasını okuyup gerçek konu etiketleri
359
+ üretir (prompt/apply perception modülünden gelir) */
360
+ report.interests = { updated: false, count: 0, labels: [] };
361
+ if (
362
+ !noLlm &&
363
+ deps.interests &&
364
+ typeof deps.interests.prompt === 'function' &&
365
+ typeof deps.interests.apply === 'function'
366
+ ) {
367
+ try {
368
+ const ip = deps.interests.prompt();
369
+ if (ip) {
370
+ const raw = await deps.llm(ip);
371
+ const ir = deps.interests.apply(raw);
372
+ if (ir && ir.ok) {
373
+ report.interests = {
374
+ updated: true,
375
+ count: ir.count || (Array.isArray(ir.labels) ? ir.labels.length : 0),
376
+ labels: Array.isArray(ir.labels) ? ir.labels : [],
377
+ };
378
+ } else {
379
+ report.notes.push('ilgi çıkarımı: model geçerli etiket üretmedi — mevcut liste korundu');
380
+ }
381
+ } else {
382
+ report.notes.push('ilgi çıkarımı: yeterli konuşma kaydı yok — atlandı');
383
+ }
384
+ } catch (e) {
385
+ report.errors.push('ilgi çıkarımı: ' + String((e && e.message) || e).slice(0, 120));
386
+ }
387
+ }
388
+
357
389
  /* 4) bağlam raporu + journal dosyası */
358
390
  const scopeLabel = deps.sinceIso
359
391
  ? dayKey(deps.sinceIso) + ' ' + String(deps.sinceIso).slice(11, 16) + ' → ' + dayKey(now) + ' ' + String(now.toTimeString ? now.toTimeString().slice(0, 5) : '')
@@ -365,6 +397,9 @@ async function run(deps) {
365
397
  '## Bugün öğrendiklerim\n' +
366
398
  (report.learnings.length ? report.learnings.map((l) => '- ' + l).join('\n') + '\n' : '- (kayıt yok)\n') +
367
399
  (report.factsAdded ? '\nHafızaya eklenen kalıcı bilgi: ' + report.factsAdded + ' madde\n' : '') +
400
+ (report.interests && report.interests.updated
401
+ ? '\n## İlgi alanları (günlük çıkarım)\n' + report.interests.labels.map((l) => '- ' + l).join('\n') + '\n'
402
+ : '') +
368
403
  '\n## Bellek bakımı\n' +
369
404
  '- Kayıt: ' + m.before + ' → ' + m.after + ' (düşülen ' + m.dropped + ', ' + m.merged + ' kayıt birleşti)\n' +
370
405
  '- Boyut: ' + m.charsBefore + ' → ' + m.charsAfter + ' char (~' + m.tokensSaved + ' token tasarruf)\n' +
@@ -21,6 +21,7 @@ const DEFAULTS = {
21
21
  notifyTarget: '', // bildirim hedefi: '' = bağlı entegrasyonlar | whatsapp | telegram | discord
22
22
  filterModel: '', // ucuz filtre modeli ('provider::model'); boşsa ana model
23
23
  interests: '', // kullanıcı ilgi alanları (relevance ağırlığı)
24
+ behavior: '', // AJAN DAVRANIŞI: proaktif mesajlarda nasıl konuşacağı (kullanıcı yazar)
24
25
  newsTopics: '', // Google News RSS sorguları (virgülle)
25
26
  weights: { importance: 0.40, relevance: 0.25, urgency: 0.20, novelty: 0.15 },
26
27
  };
@@ -56,6 +57,7 @@ function mergeCfg(raw) {
56
57
  : '',
57
58
  filterModel: String(r.filterModel || '').trim(),
58
59
  interests: String(r.interests || '').slice(0, 400),
60
+ behavior: String(r.behavior || '').slice(0, 800),
59
61
  newsTopics: String(r.newsTopics || '').slice(0, 400),
60
62
  weights: {
61
63
  importance: clampW(w.importance, DEFAULTS.weights.importance),
@@ -94,10 +96,11 @@ function saveState(st) {
94
96
  }
95
97
 
96
98
  /* ---------- empati hafızası: sohbet kaydı + öğrenilen ilgi alanları ----------
97
- Sohbet akışı (chat/WA/TG/DC) main tarafından buraya düşürülür; kullanıcı
98
- mesajlarından anlamlı kelimeler toplanır, sık geçenler "öğrenilen ilgi
99
- alanı" olur. Tarama filtresi ve bildirim mesajı bu hafızayla kişiselleşir:
100
- elle girilen ilgi alanları + öğrenilenler birlikte prompta girer. */
99
+ Sohbet akışı (chat/WA/TG/DC) main tarafından buraya düşürülür. İlgi
100
+ alanları artık KELİME SAYIMIYLA değil, gece yansımasında (günde bir)
101
+ modelin konuşma hafızasından LLM ÇIKARIMIYLA üretilir. Tarama filtresi
102
+ ve bildirim mesajı bu hafızayla kişiselleşir: elle girilen ilgi alanları
103
+ + çıkarılanlar birlikte prompta girer. */
101
104
 
102
105
  const CHAT_CAP = 120; // son sohbet kaydı tavanı
103
106
  const INTEREST_CAP = 24; // öğrenilen ilgi etiketi tavanı
@@ -109,22 +112,6 @@ function memFold(s) {
109
112
  return String(s || '').toLowerCase().replace(/[çğıöşüâîû]/g, (ch) => TR_FOLD_MEM[ch] || ch);
110
113
  }
111
114
 
112
- /* gereksiz kelimeler — ilgi alanı sayılmaz. Set FOLD'LU kurulur: kelimeler
113
- zaten memFold ile ASCII'ye indirgendikten sonra arandığı için 'bugün' gibi
114
- stopword'ler fold'lu haliyle ('bugun') listede olmalı. */
115
- const MEM_STOP_RAW =
116
- 'the and for with this that have from was are were been will would could should ' +
117
- 'bir bu şu o ve ile için gibi ama fakat çok daha en ne mi mı mu mü de da ki ise ya ' +
118
- 'veya yani bana bize sana ona beni bizi seni onun benim bizim senin onları var yok ' +
119
- 'olur olan olarak nasıl neden niye şey şeyi her hem acaba lazım gerek evet hayır ' +
120
- 'tamam tmm kanka abi hocam merhaba selam hey lütfen teşekkür sağol eyvallah bugün ' +
121
- 'yarın dün şimdi sonra önce hala henüz yine tekrar biraz az kadar çünkü eğer ' +
122
- 'yapabilir misin musun isterim istiyorum lazım gerekiyor beast asistan önemli ' +
123
- 'hakkında üzerine konuştuk konuşuyoruz konuşmak kendi aynı başka diğer belki ' +
124
- 'sanırım galiba gerekli olsun olmuyor oluyor bakalım biriyle hangi kim nerede';
125
-
126
- const MEM_STOP = new Set(MEM_STOP_RAW.split(/\s+/).map((w) => memFold(w)).filter(Boolean));
127
-
128
115
  function chatFile() {
129
116
  return path.join(beastRoot(), 'perception', 'chatmem.json');
130
117
  }
@@ -132,12 +119,22 @@ function chatFile() {
132
119
  function loadChatMem() {
133
120
  try {
134
121
  const r = JSON.parse(fs.readFileSync(chatFile(), 'utf8'));
135
- return {
122
+ const cm = {
136
123
  chats: Array.isArray(r.chats) ? r.chats : [],
137
124
  learned: r.learned && typeof r.learned === 'object' ? r.learned : {},
125
+ learnedSource: String(r.learnedSource || ''),
126
+ learnedAt: r.learnedAt || null,
138
127
  };
128
+ /* TEK SEFERLİK GÖÇ: eski kelime-frekans öğrenici çöp etiket üretiyordu
129
+ ("whatsapp", "gonderen", telefon numarası…). İlgi alanları artık gece
130
+ yansımasında LLM çıkarımı — eski kayıtlar ilk yüklemede temizlenir. */
131
+ if (cm.learnedSource !== 'llm' && Object.keys(cm.learned).length) {
132
+ cm.learned = {};
133
+ try { saveChatMem(cm); } catch {}
134
+ }
135
+ return cm;
139
136
  } catch {
140
- return { chats: [], learned: {} };
137
+ return { chats: [], learned: {}, learnedSource: '', learnedAt: null };
141
138
  }
142
139
  }
143
140
 
@@ -165,34 +162,72 @@ function rememberConversation(text, channel) {
165
162
  }
166
163
  }
167
164
 
168
- /* Kullanıcı mesajından ilgi alanı öğren: fold + stopword temizliği, sayaca yaz */
169
- function learnInterests(text) {
165
+ /* ---------- ilgi alanı çıkarımı (gece yansımasında, günde bir, LLM) ----------
166
+ Kelime-frekans sayımı YOK — taşıma etiketleri ("gönderen", kanal adı,
167
+ telefon) ve rastgele kelimeler ilgi sanılıyordu. Bunun yerine biriken
168
+ konuşma hafızasını model okur, gerçek KONU etiketleri çıkarır. */
169
+
170
+ const INTEREST_INFER_MAX_CHATS = 80;
171
+
172
+ function interestPrompt() {
173
+ const cm = loadChatMem();
174
+ const items = cm.chats.slice(-INTEREST_INFER_MAX_CHATS);
175
+ if (items.length < 5) return ''; // veri yetersiz — çıkarıma değmez
176
+ const lines = items.map((c) => '- [' + (c.ch || 'sohbet') + '] ' + c.t).join('\n').slice(0, 9000);
177
+ return (
178
+ 'Sen bir kullanıcı profili çıkarıcısısın. Aşağıdaki son sohbet kayıtlarından ' +
179
+ 'KULLANICININ GERÇEK İLGİ ALANLARINI çıkar.\n' +
180
+ 'KURALLAR:\n' +
181
+ '- 3-12 etiket; her biri 1-3 kelimelik KONU/ALAN adı (Türkçe) — örn. "kripto para", "yapay zeka", "web geliştirme", "futbol"\n' +
182
+ '- Yalnız kullanıcının gerçekten ilgilendiği/tekrar konuştuğu konular; tek seferlik rastgele kelimeler ve sohbet dolguları ETİKET DEĞİL\n' +
183
+ '- Kişi adı, telefon numarası, kanal/uygulama adı (whatsapp, telegram), hitap sözleri (kanka, abi), istek fiilleri ("yap", "ara", "gönder") KESİNLİKLE etiket olamaz\n' +
184
+ '- Emin olamadığın konuyu yazma; az ama doğru etiket — uydurma yok\n' +
185
+ 'SADECE JSON dön: {"interests":["...","..."]}\n\n' +
186
+ '# SON KONUŞMALAR (beast = asistanın cevabı, sohbet/empati = kullanıcı tarafı)\n' + lines
187
+ );
188
+ }
189
+
190
+ /* LLM cevabından etiket listesi: sert temizlik + dedupe (fold'lu) + tavan */
191
+ function parseInterestJson(raw) {
170
192
  try {
171
- const toks = (memFold(text).match(/[a-z0-9_+#.]{3,}/g) || [])
172
- .filter((w) => !MEM_STOP.has(w) && !/^\d+$/.test(w))
173
- .slice(0, 12);
174
- if (!toks.length) return false;
175
- const cm = loadChatMem();
176
- const now = Date.now();
177
- for (const w of toks) {
178
- const cur = cm.learned[w] || { c: 0, at: 0 };
179
- cur.c++;
180
- cur.at = now;
181
- cm.learned[w] = cur;
182
- }
183
- /* tavana in: en sık + en taze etiketler kalır */
184
- const keys = Object.keys(cm.learned);
185
- if (keys.length > INTEREST_CAP) {
186
- keys.sort((a, b) => (cm.learned[b].c - cm.learned[a].c) || (cm.learned[b].at - cm.learned[a].at));
187
- for (const k of keys.slice(INTEREST_CAP)) delete cm.learned[k];
193
+ const t = String(raw || '').replace(/```(?:json)?/gi, '');
194
+ const a = t.indexOf('{');
195
+ const b = t.lastIndexOf('}');
196
+ if (a < 0 || b <= a) return [];
197
+ const obj = JSON.parse(t.slice(a, b + 1));
198
+ const arr = Array.isArray(obj && obj.interests) ? obj.interests : [];
199
+ const seen = new Set();
200
+ const out = [];
201
+ for (const x of arr) {
202
+ const w = String(x || '').replace(/\s+/g, ' ').trim().slice(0, 40);
203
+ if (w.length < 3) continue;
204
+ if (/\d{4,}/.test(w)) continue; // telefon/tarih parçası etiket değil
205
+ const k = memFold(w);
206
+ if (!k || seen.has(k)) continue;
207
+ seen.add(k);
208
+ out.push(w);
209
+ if (out.length >= INTEREST_CAP) break;
188
210
  }
189
- saveChatMem(cm);
190
- return true;
211
+ return out;
191
212
  } catch {
192
- return false;
213
+ return [];
193
214
  }
194
215
  }
195
216
 
217
+ /* Çıkarılan etiketlerle öğrenilen listeyi TAMAMEN değiştir (günlük taze profil) */
218
+ function applyInferredInterests(llmText) {
219
+ const labels = parseInterestJson(llmText);
220
+ if (!labels.length) return { ok: false, labels: [] };
221
+ const cm = loadChatMem();
222
+ const now = Date.now();
223
+ cm.learned = {};
224
+ for (const w of labels) cm.learned[w] = { c: 1, at: now };
225
+ cm.learnedSource = 'llm';
226
+ cm.learnedAt = new Date(now).toISOString();
227
+ saveChatMem(cm);
228
+ return { ok: true, labels: [...labels], count: labels.length };
229
+ }
230
+
196
231
  /* Elle girilen + öğrenilen ilgi alanları birleşik (promptlar bunu kullanır) */
197
232
  function combinedInterests(cfg) {
198
233
  const manual = String((cfg && cfg.interests) || '').trim();
@@ -283,6 +318,7 @@ function normalizeRaw(item) {
283
318
  type,
284
319
  title,
285
320
  detail: String((item && item.detail) || '').slice(0, 300),
321
+ url: String((item && item.url) || '').slice(0, 600),
286
322
  sources: [(item && item.source) || type],
287
323
  scores: {},
288
324
  priority: 0,
@@ -313,7 +349,11 @@ async function fetchNews(topics) {
313
349
  const title = t ? String(t[1]).replace(/\s+/g, ' ').trim() : '';
314
350
  if (!title) continue;
315
351
  count++;
316
- out.push({ type: 'news', title, detail: 'Google News · ' + q, source: 'news:' + q });
352
+ /* haberin GERÇEK linki: kullanıcı "kaynak neymiş?" diye sorduğunda
353
+ Beast'in haberi bulabilmesi için bildirimle birlikte taşınır */
354
+ const l = /<link>(?:<!\[CDATA\[)?([\s\S]*?)(?:\]\]>)?<\/link>/.exec(m[0]);
355
+ const url = l ? String(l[1]).trim() : '';
356
+ out.push({ type: 'news', title, detail: 'Google News · ' + q, source: 'news:' + q, url });
317
357
  }
318
358
  } catch {}
319
359
  }
@@ -399,16 +439,21 @@ function parseFilterJson(text) {
399
439
  const COMPOSE_SYSTEM =
400
440
  'Beast adlı yerel yardımcı için proaktif bildirim metni yazarsın. Türkçe, samimi "kanka" tonunda, ' +
401
441
  'EN FAZLA 2 kısa cümle: ne olduğunu soyutla, neden önemli olabileceğini söyle, istersen tek kısa soru sor. ' +
402
- 'Alarm spam\'i yok; başlık/emoji/Markdown ekleme, yalnız düz metin yaz.';
442
+ 'Alarm spam\'i yok; başlık/emoji/Markdown ekleme, yalnız düz metin yaz. ' +
443
+ 'LINK URL\'sini KENDİN YAZMA — kaynak linki sistem tarafından mesajın sonuna OTOMATİK eklenir. ' +
444
+ 'Promptta AJAN DAVRANIŞI kuralı varsa ona UY; çelişirse davranış kuralı kazanır.';
403
445
 
404
446
  function composePrompt(ev, cfg) {
405
447
  const ctx = chatContextLine(5);
448
+ const behavior = String((cfg && cfg.behavior) || '').trim();
406
449
  return (
407
450
  'OLAY: ' + ev.title + '\n' +
408
451
  (ev.detail ? 'DETAY: ' + ev.detail + '\n' : '') +
409
452
  'KAYNAK: ' + ev.source + '\n' +
453
+ (ev.url ? 'LINK: ' + ev.url + '\n' : '') +
410
454
  'ÖNCELİK: ' + ev.priority + '/100 · ' + (ev.reason || '') + '\n' +
411
455
  'KULLANICI İLGİLERİ: ' + (combinedInterests(cfg) || '(belirtilmemiş)') + '\n' +
456
+ 'AJAN DAVRANIŞI (bu kurallara UY): ' + (behavior || '(özel kural yok — varsayılan samimi kanka tonu)') + '\n' +
412
457
  (ctx ? ctx + '\n' : '') +
413
458
  '\nBu olay için kullanıcıya gönderilecek proaktif kısa mesajı yaz.'
414
459
  );
@@ -580,6 +625,7 @@ function listEvents(limit) {
580
625
  type: e.type,
581
626
  title: e.title,
582
627
  detail: e.detail,
628
+ url: e.url || '',
583
629
  priority: e.priority,
584
630
  scores: e.scores || {},
585
631
  reason: e.reason,
@@ -602,7 +648,9 @@ module.exports = {
602
648
  listEvents,
603
649
  lastRunAt,
604
650
  rememberConversation,
605
- learnInterests,
651
+ interestPrompt,
652
+ applyInferredInterests,
653
+ parseInterestJson,
606
654
  combinedInterests,
607
655
  chatContextLine,
608
656
  memSnapshot,