beast-agent 0.15.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 (62) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +94 -0
  4. package/assets/app.ico +0 -0
  5. package/assets/tray.png +0 -0
  6. package/bin/beast-agent.js +23 -0
  7. package/config.example.yaml +26 -0
  8. package/eng.traineddata +0 -0
  9. package/package.json +101 -0
  10. package/scripts/swap-electron.js +40 -0
  11. package/src/agent/bus.js +384 -0
  12. package/src/agent/computeruse.js +200 -0
  13. package/src/agent/config.js +284 -0
  14. package/src/agent/engine.js +3118 -0
  15. package/src/agent/kb.js +123 -0
  16. package/src/agent/llm.js +219 -0
  17. package/src/agent/logger.js +90 -0
  18. package/src/agent/memory.js +383 -0
  19. package/src/agent/pdf.js +20 -0
  20. package/src/agent/scripts/__pycache__/websearch.cpython-312.pyc +0 -0
  21. package/src/agent/scripts/news.py +113 -0
  22. package/src/agent/scripts/websearch.py +225 -0
  23. package/src/agent/skills.js +522 -0
  24. package/src/agent/tokens.js +39 -0
  25. package/src/agent/tools.js +911 -0
  26. package/src/agent/usage.js +125 -0
  27. package/src/agent/watchers.js +312 -0
  28. package/src/agent/watext.js +75 -0
  29. package/src/agent/whatsapp.js +494 -0
  30. package/src/cron.js +245 -0
  31. package/src/main.js +3622 -0
  32. package/src/preload.js +122 -0
  33. package/src/renderer/browserPreload.js +73 -0
  34. package/src/renderer/i18n.js +757 -0
  35. package/src/renderer/index.html +227 -0
  36. package/src/renderer/renderer.js +3250 -0
  37. package/src/renderer/style.css +1559 -0
  38. package/tests/approval.test.js +56 -0
  39. package/tests/bg-jobs.test.js +315 -0
  40. package/tests/bus.test.js +46 -0
  41. package/tests/computeruse-context.test.js +69 -0
  42. package/tests/cron.test.js +95 -0
  43. package/tests/engine.test.js +140 -0
  44. package/tests/eval.test.js +91 -0
  45. package/tests/fallout.test.js +188 -0
  46. package/tests/llm.test.js +56 -0
  47. package/tests/memory.test.js +33 -0
  48. package/tests/memoryloop.test.js +26 -0
  49. package/tests/notegen.test.js +63 -0
  50. package/tests/owner.test.js +32 -0
  51. package/tests/python.test.js +75 -0
  52. package/tests/scenarios.json +81 -0
  53. package/tests/sessioncode.test.js +50 -0
  54. package/tests/setup.js +9 -0
  55. package/tests/tokens.test.js +42 -0
  56. package/tests/tools.test.js +84 -0
  57. package/tests/usage.test.js +44 -0
  58. package/tests/v11.test.js +76 -0
  59. package/tests/watchers.test.js +206 -0
  60. package/tests/watext.test.js +53 -0
  61. package/tests/whatsapp.test.js +103 -0
  62. package/tests/wherewasi.test.js +40 -0
@@ -0,0 +1,383 @@
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
+ const ALLOWED_FILES = ['SOUL.md', 'MEMORY.md', 'USER.md'];
88
+
89
+ function save(file, content) {
90
+ try {
91
+ if (!ALLOWED_FILES.includes(file)) return { ok: false, error: 'bad file' };
92
+ ensure();
93
+ fs.writeFileSync(path.join(memDir(), file), String(content ?? ''), 'utf8');
94
+ return { ok: true };
95
+ } catch (e) {
96
+ return { ok: false, error: String((e && e.message) || e) };
97
+ }
98
+ }
99
+
100
+ /* ---------- retrieval ---------- */
101
+
102
+ const TR_FOLD = { ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u' };
103
+
104
+ function fold(s) {
105
+ return String(s || '')
106
+ .toLowerCase()
107
+ .replace(/[çğıöşüâîû]/g, (ch) => TR_FOLD[ch] || ch);
108
+ }
109
+
110
+ function tokenize(q) {
111
+ return (fold(q).match(/[a-z0-9_+#.]+/g) || []).filter((w) => w.length >= 3);
112
+ }
113
+
114
+ function entries() {
115
+ return read('MEMORY.md')
116
+ .split('\n')
117
+ .map((l) => l.replace(/^[-*]\s*/, '').trim())
118
+ .filter(Boolean);
119
+ }
120
+
121
+ function scoreEntry(entryText, qTokens) {
122
+ if (!qTokens.length) return 0;
123
+ const hay = fold(entryText);
124
+ let hits = 0;
125
+ for (const t of new Set(qTokens)) {
126
+ if (hay.includes(t)) hits++;
127
+ }
128
+ return hits / Math.sqrt(qTokens.length);
129
+ }
130
+
131
+ /* Sorguyla ilgili en güçlü N kayıt + en son eklenen M kayıt (sıra korunur). */
132
+ function relevantFor(query, { maxRelevant = 6, maxRecent = 4, charCap = 2400 } = {}) {
133
+ const list = entries();
134
+ if (!list.length) return '';
135
+ const qTokens = tokenize(query);
136
+ const picked = new Set();
137
+ if (qTokens.length) {
138
+ const scored = list
139
+ .map((text, i) => ({ i, text, score: scoreEntry(text, qTokens) }))
140
+ .filter((r) => r.score > 0)
141
+ .sort((a, b) => b.score - a.score)
142
+ .slice(0, maxRelevant);
143
+ for (const r of scored) picked.add(r.i);
144
+ }
145
+ for (let i = list.length - 1; i >= 0 && picked.size < maxRelevant + maxRecent; i--) {
146
+ picked.add(i);
147
+ }
148
+ let out = '';
149
+ for (const i of [...picked].sort((a, b) => a - b)) {
150
+ const line = '- ' + list[i];
151
+ if (out.length + line.length > charCap) break;
152
+ out += (out ? '\n' : '') + line;
153
+ }
154
+ return out;
155
+ }
156
+
157
+ function search(query, limit = 8) {
158
+ const list = entries();
159
+ const qTokens = tokenize(query);
160
+ if (!list.length) return [];
161
+ let rows;
162
+ if (!qTokens.length) {
163
+ rows = list.map((text, i) => ({ text, score: 0, i })).slice(-limit).reverse();
164
+ } else {
165
+ rows = list
166
+ .map((text, i) => ({ text, score: Number(scoreEntry(text, qTokens).toFixed(3)), i }))
167
+ .filter((r) => r.score > 0)
168
+ .sort((a, b) => b.score - a.score)
169
+ .slice(0, limit);
170
+ }
171
+ return rows.map(({ text, score }) => ({ score, text }));
172
+ }
173
+
174
+ /* ---------- kalıcı kurallar (#3 düzeltme hattı) ----------
175
+ Kullanıcının agent'a söylediği "artık hep böyle yap" maddeleri.
176
+ MEMORY.md'yi şişirmeden ayrı dosyada durur, her prompta girer. */
177
+
178
+ function rulesFile() {
179
+ return path.join(beastRoot(), 'rules.md');
180
+ }
181
+
182
+ function listRules() {
183
+ try {
184
+ return fs
185
+ .readFileSync(rulesFile(), 'utf8')
186
+ .split('\n')
187
+ .map((l) => l.replace(/^-\s*/, '').trim())
188
+ .filter(Boolean);
189
+ } catch {
190
+ return [];
191
+ }
192
+ }
193
+
194
+ function addRule(text) {
195
+ const t = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 300);
196
+ if (!t) return { ok: false, error: 'kural metni boş' };
197
+ if (listRules().some((r) => r.toLowerCase() === t.toLowerCase())) {
198
+ return { ok: true, duplicate: true };
199
+ }
200
+ try {
201
+ fs.mkdirSync(beastRoot(), { recursive: true });
202
+ fs.appendFileSync(rulesFile(), '- ' + t + '\n');
203
+ return { ok: true };
204
+ } catch (e) {
205
+ return { ok: false, error: String((e && e.message) || e) };
206
+ }
207
+ }
208
+
209
+ function removeRule(textOrIndex) {
210
+ const rules = listRules();
211
+ let idx = -1;
212
+ if (/^\d+$/.test(String(textOrIndex))) idx = Number(textOrIndex) - 1;
213
+ else idx = rules.findIndex((r) => r.toLowerCase() === String(textOrIndex).trim().toLowerCase());
214
+ if (idx < 0 || idx >= rules.length) return { ok: false, error: 'kural bulunamadı' };
215
+ rules.splice(idx, 1);
216
+ try {
217
+ fs.writeFileSync(rulesFile(), rules.map((r) => '- ' + r).join('\n') + (rules.length ? '\n' : ''));
218
+ return { ok: true };
219
+ } catch (e) {
220
+ return { ok: false, error: String((e && e.message) || e) };
221
+ }
222
+ }
223
+
224
+ /* ---------- bellek hijyeni (#5) ----------
225
+ MEMORY.md: dedup + eskime; periyodik çağrıyla şişme engellenir. */
226
+
227
+ const MEMORY_CAP = 400; // maksimum kayıt sayısı
228
+ const MEMORY_MAX_AGE_DAYS = 120; // hiç skor üretmeyen kayıtların ömrü
229
+
230
+ /** Duplike + çok eski kayıtları temizler; silinen sayısını döner */
231
+ function hygiene({ maxAgeDays = MEMORY_MAX_AGE_DAYS, cap = MEMORY_CAP } = {}) {
232
+ const list = entries();
233
+ if (!list.length) return { ok: true, removed: 0, reason: 'boş' };
234
+ const seen = new Set();
235
+ const kept = [];
236
+ let dupes = 0;
237
+ for (const l of list) {
238
+ const key = fold(l).replace(/[^a-z0-9]+/g, ' ').trim();
239
+ if (seen.has(key)) {
240
+ dupes++;
241
+ continue;
242
+ }
243
+ seen.add(key);
244
+ kept.push(l);
245
+ }
246
+ let aged = 0;
247
+ if (kept.length > cap || maxAgeDays > 0) {
248
+ /* tarih etiketli olmayanlar en eski kabul edilir (append-only başlangıç) */
249
+ const cut = Date.now() - maxAgeDays * 86400000;
250
+ const filtered = [];
251
+ /* kronolojik varsayım: dosya sırası = ekleme sırası */
252
+ const overflow = Math.max(0, kept.length - cap);
253
+ kept.forEach((l, idx) => {
254
+ const m = l.match(/\b(\d{4}-\d{2}-\d{2})\b/);
255
+ const tooOld = m && new Date(m[1]).getTime() < cut;
256
+ if (tooOld && idx < kept.length - 20) { // son 20 kayıt asla yaşla silinmez
257
+ aged++;
258
+ return;
259
+ }
260
+ if (overflow > 0 && idx < overflow) {
261
+ // kap aşımı: en eskilerden düş ama yine de son 20 korunur
262
+ if (idx < kept.length - 20) {
263
+ aged++;
264
+ return;
265
+ }
266
+ }
267
+ filtered.push(l);
268
+ });
269
+ return writeKept(filtered, dupes + aged);
270
+ }
271
+ return writeKept(kept, dupes);
272
+ }
273
+
274
+ function writeKept(kept, removedCount) {
275
+ try {
276
+ fs.writeFileSync(
277
+ path.join(memDir(), 'MEMORY.md'),
278
+ kept.map((l) => '- ' + l).join('\n') + (kept.length ? '\n' : '')
279
+ );
280
+ return { ok: true, removed: removedCount, remaining: kept.length };
281
+ } catch (e) {
282
+ return { ok: false, error: String((e && e.message) || e) };
283
+ }
284
+ }
285
+
286
+ /* ---------- #12 kişilik kalibrasyonu ----------
287
+ Kullanıcının konuşma tarzından üslup imzası çıkarır, SOUL.md'ye ekler.
288
+ İlk hafta mesajlar birikince "dijital ikiz" ton yakalanır. */
289
+
290
+ function personaFile() {
291
+ return path.join(memDir(), 'persona.json');
292
+ }
293
+
294
+ function loadPersona() {
295
+ try {
296
+ return JSON.parse(fs.readFileSync(personaFile(), 'utf8'));
297
+ } catch {
298
+ return { samples: [], calibrated: false };
299
+ }
300
+ }
301
+
302
+ function savePersona(p) {
303
+ try {
304
+ fs.mkdirSync(memDir(), { recursive: true });
305
+ fs.writeFileSync(personaFile(), JSON.stringify(p, null, 2));
306
+ } catch {}
307
+ }
308
+
309
+ /** Kullanıcı mesajı → üslup örnek havuzuna ekle (max 40) */
310
+ function addStyleSample(text) {
311
+ const t = String(text || '').trim();
312
+ if (t.length < 8 || t.length > 300) return; // çok kısa/uzun örnek değmez
313
+ if (/^(\/|http)/.test(t)) return; // komut/link değil
314
+ const p = loadPersona();
315
+ if (p.samples.some((s) => s.toLowerCase() === t.toLowerCase())) return;
316
+ p.samples.push(t);
317
+ if (p.samples.length > 40) p.samples = p.samples.slice(-40);
318
+ savePersona(p);
319
+ }
320
+
321
+ const STYLE_HINTS = [
322
+ ['emoji', /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u],
323
+ ['kısa cümle', /^(.{1,60}(\.|$)){1,2}/],
324
+ ['türkçe kısaltma', /\b(tmm|tamam|tamamsa|olur|olmz|yok|bşka|hemen|acele|asap)\b/i],
325
+ ['seslenme', /\b(kanka|abi|abey|hocam|kanka)\b/i],
326
+ ['üzgün ünlem', /!{1,}$/m],
327
+ ['soru ağırlıklı', /\?/],
328
+ ];
329
+
330
+ function analyzeStyle(samples) {
331
+ const counts = {};
332
+ for (const [, re] of STYLE_HINTS) {
333
+ for (const s of samples) if (re.test(s)) counts[re.source] = (counts[re.source] || 0) + 1;
334
+ }
335
+ // basit: hangi hintler %35+ örnekte görünüyor
336
+ const n = Math.max(1, samples.length);
337
+ return STYLE_HINTS.filter(([, re]) => (counts[re.source] || 0) / n >= 0.35).map(([name]) => name);
338
+ }
339
+
340
+ /** Yeterli örnek biriktiyse SOUL.md'ye üslup paragrafı yaz/revize et */
341
+ function calibratePersona() {
342
+ const p = loadPersona();
343
+ if (p.samples.length < 20) return { ok: false, reason: 'yetersiz örnek', have: p.samples.length, need: 20 };
344
+ const traits = analyzeStyle(p.samples);
345
+ const style =
346
+ '# KULLANICI ÜSLUBU (sohbetlerinden kalibre edildi — bu tonda konuş)\n' +
347
+ (traits.length
348
+ ? '- Gözlemlenen özellikler: ' + traits.join(', ') + '\n'
349
+ : '- Nötr ve ölçülü bir üslup\n') +
350
+ '- Örnek kullanıcı cümleleri:\n' +
351
+ p.samples.slice(-6).map((s) => ' • "' + s.slice(0, 90) + '"').join('\n');
352
+ try {
353
+ const soulPath = path.join(memDir(), 'SOUL.md');
354
+ let soul = fs.readFileSync(soulPath, 'utf8');
355
+ soul = soul.replace(/# KULLANICI ÜSLUBU[\s\S]*?(?=\n# |\n*$)/g, '').trimEnd();
356
+ fs.writeFileSync(soulPath, soul + '\n\n' + style + '\n');
357
+ p.calibrated = true;
358
+ p.calibratedAt = new Date().toISOString();
359
+ savePersona(p);
360
+ return { ok: true, traits };
361
+ } catch (e) {
362
+ return { ok: false, error: String((e && e.message) || e) };
363
+ }
364
+ }
365
+
366
+ module.exports = {
367
+ beastRoot,
368
+ memDir,
369
+ loadAll,
370
+ append,
371
+ save,
372
+ entries,
373
+ relevantFor,
374
+ search,
375
+ tokenize,
376
+ scoreEntry,
377
+ listRules,
378
+ addRule,
379
+ removeRule,
380
+ hygiene,
381
+ addStyleSample,
382
+ calibratePersona,
383
+ };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ /* Beast PDF yardımcısı: pdf-parse v2 ile düz metin çıkarımı.
4
+ pdf-parse kendi pdfjs-dist@5.4.296 kopyasını kullanır (nested). Aynı process'te
5
+ başka bir pdfjs sürümü yükleyen paket (örn. pdf-to-img) açılırsa globalThis
6
+ worker cache çakışmasından "API version ... does not match Worker version"
7
+ hatası çıkar — o yüzden render/OCR işleri AYRI node süreçlerinde yapılır
8
+ (ayrıntı: pdf skill'i). */
9
+
10
+ async function extract(buf) {
11
+ const { PDFParse } = require('pdf-parse');
12
+ const parser = new PDFParse({ data: new Uint8Array(buf) });
13
+ try {
14
+ return await parser.getText();
15
+ } finally {
16
+ await parser.destroy();
17
+ }
18
+ }
19
+
20
+ module.exports = { extract };
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Beast haber toplayıcı — yalnızca standart kütüphane (requests yok).
4
+
5
+ Kaynaklar: NTV, CNN Türk, Habertürk, BBC Türkçe, Anadolu Ajansı.
6
+ Kullanım:
7
+ python news.py # her kaynaktan 8 başlık
8
+ python news.py --limit 3 # her kaynaktan 3 başlık
9
+ python news.py --feed ntv # tek kaynak (--json ile birlikte güzel)
10
+ python news.py --json # makine okunur JSON satırları
11
+ Çıktı formatı (satır başına): SAAT | KAYNAK | Başlık | Link
12
+ """
13
+ import argparse
14
+ import json
15
+ import re
16
+ import ssl
17
+ import sys
18
+ import urllib.request
19
+ import xml.etree.ElementTree as ET
20
+
21
+ try: # konsol kod sayfasından bağımsız temiz UTF-8 çıktı
22
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
23
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
24
+ except Exception:
25
+ pass
26
+
27
+ FEEDS = {
28
+ "ntv": "https://www.ntv.com.tr/gundem.rss",
29
+ "cnnturk": "https://www.cnnturk.com/rss/rss.aspx?rss=1",
30
+ "haberturk": "https://www.haberturk.com/rss",
31
+ "bbcturkce": "https://feeds.bbci.co.uk/turkce/rss.xml",
32
+ "aa": "https://www.aa.com.tr/tr/rss/default?cat=guncel",
33
+ }
34
+
35
+ TAG_RE = re.compile(r"<[^>]+>")
36
+ CTX = ssl.create_default_context()
37
+ UA = {"User-Agent": "Mozilla/5.0 BeastAgent news.py"}
38
+
39
+
40
+ def fetch(url, timeout=15):
41
+ req = urllib.request.Request(url, headers=UA)
42
+ with urllib.request.urlopen(req, timeout=timeout, context=CTX) as r:
43
+ return r.read()
44
+
45
+
46
+ def strip_html(s):
47
+ return TAG_RE.sub(" ", s or "").replace("&amp;", "&").replace("&quot;", '"').replace(
48
+ "&#39;", "'"
49
+ ).replace("&lt;", "<").replace("&gt;", ">").strip()
50
+
51
+
52
+ def text_of(node):
53
+ return strip_html(node.text if node is not None and node.text else "")
54
+
55
+
56
+ def parse_feed(name, url):
57
+ """RSS/Atom'u iki namespace'te de dener; (saat, kaynak, başlık, link) döner."""
58
+ raw = fetch(url)
59
+ root = ET.fromstring(raw)
60
+ items = []
61
+ for it in root.iter():
62
+ tag = it.tag.rsplit("}", 1)[-1]
63
+ if tag not in ("item", "entry"):
64
+ continue
65
+ title = link = date = ""
66
+ for ch in it:
67
+ t = ch.tag.rsplit("}", 1)[-1]
68
+ if t == "title":
69
+ title = text_of(ch)
70
+ elif t == "link":
71
+ link = (ch.get("href") or "").strip() or text_of(ch)
72
+ elif t in ("pubDate", "published", "updated", "date"):
73
+ date = text_of(ch)[:16]
74
+ if title:
75
+ items.append((date or "-", name.upper(), title[:180], link))
76
+ return items
77
+
78
+
79
+ def main():
80
+ ap = argparse.ArgumentParser(description="Beast RSS haber toplayıcı")
81
+ ap.add_argument("--limit", type=int, default=8, help="kaynak başına başlık sayısı")
82
+ ap.add_argument("--feed", default="", help="tek kaynak anahtarı (ntv,cnnturk,haberturk,bbcturkce,aa)")
83
+ ap.add_argument("--json", action="store_true", help="JSON Lines çıktısı")
84
+ a = ap.parse_args()
85
+
86
+ wanted = {a.feed.strip().lower()} if a.feed else set(FEEDS)
87
+ unknown = wanted - set(FEEDS)
88
+ if unknown:
89
+ print(f"[news.py] bilinmeyen kaynak: {', '.join(unknown)}", file=sys.stderr)
90
+
91
+ rows, errors = [], []
92
+ for name in FEEDS:
93
+ if name not in wanted:
94
+ continue
95
+ try:
96
+ rows.extend(parse_feed(name, FEEDS[name])[: max(0, a.limit)])
97
+ except Exception as e: # tek kaynak düşsün, diğerleri devam
98
+ errors.append(f"{name}: {e}")
99
+
100
+ if a.json:
101
+ for r in rows:
102
+ print(json.dumps({"time": r[0], "source": r[1], "title": r[2], "link": r[3]}, ensure_ascii=False))
103
+ else:
104
+ for r in rows:
105
+ print(f"{r[0]} | {r[1]} | {r[2]}\n {r[3]}")
106
+
107
+ for e in errors:
108
+ print(f"[news.py] hata {e}", file=sys.stderr)
109
+ print(f"[news.py] {len(rows)} haber, {len(errors)} kaynak hatası", file=sys.stderr)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()