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,155 +1,155 @@
1
- 'use strict';
2
-
3
- /* TELEGRAM KÖPRÜSÜ (FEATURE 3)
4
- WhatsApp köprüsünün Telegram hali — bağımlılık yok (saf Node https):
5
- - Bot API long polling (getUpdates) ile gelen mesajlar onIncoming'e düşer
6
- - send(chatId, text) cevap döner; 4096 karakter sınırında bölerek gönderir
7
- - Aynı allow list mantığı: main tarafındaki tgFind() listesindeki kişilere cevap verir */
8
-
9
- const https = require('https');
10
-
11
- const API_BASE = 'https://api.telegram.org/bot';
12
- const SEND_CHUNK = 3800; // Telegram mesaj sınırı 4096 — güvenli pay
13
-
14
- class TelegramBridge {
15
- constructor({ token, emit, onIncoming }) {
16
- this.token = String(token || '').trim();
17
- this.emit = emit || (() => {});
18
- this.onIncoming = onIncoming || null;
19
- this.connected = false;
20
- this.stopping = false;
21
- this.status = 'disconnected';
22
- this.offset = 0;
23
- this.me = null;
24
- this._req = null; // aktif long-poll isteği (iptal için)
25
- }
26
-
27
- _setStatus(status, user) {
28
- this.status = status;
29
- this.connected = status === 'connected';
30
- this.emit({ type: 'status', status, user: user || null });
31
- }
32
-
33
- /* Bot API çağrısı — JSON POST, promise sarmalı */
34
- api(method, body = {}, opts = {}) {
35
- return new Promise((resolve, reject) => {
36
- const payload = JSON.stringify(body);
37
- const req = https.request(
38
- `${API_BASE}${this.token}/${method}`,
39
- {
40
- method: 'POST',
41
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
42
- timeout: opts.timeout || 15000,
43
- },
44
- (res) => {
45
- let data = '';
46
- res.setEncoding('utf8');
47
- res.on('data', (c) => { data += c; });
48
- res.on('end', () => {
49
- try {
50
- const j = JSON.parse(data || '{}');
51
- if (j.ok) resolve(j.result);
52
- else reject(new Error(`telegram ${method}: ${j.description || 'hata ' + res.statusCode}`));
53
- } catch (e) {
54
- reject(new Error(`telegram ${method}: bozuk yanıt`));
55
- }
56
- });
57
- }
58
- );
59
- req.on('timeout', () => req.destroy(new Error('zaman aşımı')));
60
- req.on('error', reject);
61
- this._req = req;
62
- req.write(payload);
63
- req.end();
64
- });
65
- }
66
-
67
- async start() {
68
- if (!this.token) {
69
- this._setStatus('error');
70
- return false;
71
- }
72
- this.stopping = false;
73
- this.offset = 0;
74
- this._setStatus('connecting');
75
- try {
76
- const me = await this.api('getMe', {});
77
- this.me = me;
78
- this._setStatus('connected', '@' + (me.username || me.first_name || 'bot'));
79
- } catch (e) {
80
- this._setStatus('error');
81
- throw e;
82
- }
83
- this._pollLoop().catch(() => {});
84
- return true;
85
- }
86
-
87
- /* long polling döngüsü — bağlantı koparsa 3 sn bekleyip devam */
88
- async _pollLoop() {
89
- while (!this.stopping) {
90
- try {
91
- const updates = await this.api(
92
- 'getUpdates',
93
- { timeout: 25, offset: this.offset, allowed_updates: ['message'] },
94
- { timeout: 35000 }
95
- );
96
- if (this.stopping) break;
97
- for (const u of Array.isArray(updates) ? updates : []) {
98
- this.offset = Math.max(this.offset, (u.update_id || 0) + 1);
99
- const msg = u.message;
100
- if (!msg || !msg.text || !msg.from || msg.from.is_bot) continue;
101
- const chatId = msg.chat && msg.chat.id;
102
- if (chatId === undefined || chatId === null) continue;
103
- const payload = {
104
- text: String(msg.text).slice(0, 6000),
105
- senderId: String(msg.from.id || ''),
106
- username: String(msg.from.username || ''),
107
- senderName: String(msg.from.first_name || msg.from.username || ''),
108
- isGroup: !!(msg.chat && (msg.chat.type === 'group' || msg.chat.type === 'supergroup')),
109
- };
110
- try {
111
- if (this.onIncoming) this.onIncoming(String(chatId), payload);
112
- } catch {}
113
- }
114
- } catch (e) {
115
- if (this.stopping) break;
116
- this.emit({ type: 'poll-error', error: String((e && e.message) || e) });
117
- await new Promise((r) => setTimeout(r, 3000));
118
- }
119
- }
120
- }
121
-
122
- async stop() {
123
- this.stopping = true;
124
- try { if (this._req) this._req.destroy(new Error('stop')); } catch {}
125
- this._req = null;
126
- this.connected = false;
127
- this.status = 'disconnected';
128
- }
129
-
130
- snapshot() {
131
- return {
132
- status: this.status,
133
- user: this.me ? '@' + (this.me.username || this.me.first_name || 'bot') : null,
134
- connected: this.connected,
135
- };
136
- }
137
-
138
- /* Metin gönder — 4096 sınırı için parçalara böl */
139
- async send(chatId, text) {
140
- const t = String(text || '');
141
- if (!t.trim()) return false;
142
- const chunks = [];
143
- for (let i = 0; i < t.length; i += SEND_CHUNK) chunks.push(t.slice(i, i + SEND_CHUNK));
144
- for (const part of chunks) {
145
- await this.api('sendMessage', {
146
- chat_id: chatId,
147
- text: part,
148
- disable_web_page_preview: true,
149
- });
150
- }
151
- return true;
152
- }
153
- }
154
-
155
- module.exports = { TelegramBridge };
1
+ 'use strict';
2
+
3
+ /* TELEGRAM KÖPRÜSÜ (FEATURE 3)
4
+ WhatsApp köprüsünün Telegram hali — bağımlılık yok (saf Node https):
5
+ - Bot API long polling (getUpdates) ile gelen mesajlar onIncoming'e düşer
6
+ - send(chatId, text) cevap döner; 4096 karakter sınırında bölerek gönderir
7
+ - Aynı allow list mantığı: main tarafındaki tgFind() listesindeki kişilere cevap verir */
8
+
9
+ const https = require('https');
10
+
11
+ const API_BASE = 'https://api.telegram.org/bot';
12
+ const SEND_CHUNK = 3800; // Telegram mesaj sınırı 4096 — güvenli pay
13
+
14
+ class TelegramBridge {
15
+ constructor({ token, emit, onIncoming }) {
16
+ this.token = String(token || '').trim();
17
+ this.emit = emit || (() => {});
18
+ this.onIncoming = onIncoming || null;
19
+ this.connected = false;
20
+ this.stopping = false;
21
+ this.status = 'disconnected';
22
+ this.offset = 0;
23
+ this.me = null;
24
+ this._req = null; // aktif long-poll isteği (iptal için)
25
+ }
26
+
27
+ _setStatus(status, user) {
28
+ this.status = status;
29
+ this.connected = status === 'connected';
30
+ this.emit({ type: 'status', status, user: user || null });
31
+ }
32
+
33
+ /* Bot API çağrısı — JSON POST, promise sarmalı */
34
+ api(method, body = {}, opts = {}) {
35
+ return new Promise((resolve, reject) => {
36
+ const payload = JSON.stringify(body);
37
+ const req = https.request(
38
+ `${API_BASE}${this.token}/${method}`,
39
+ {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
42
+ timeout: opts.timeout || 15000,
43
+ },
44
+ (res) => {
45
+ let data = '';
46
+ res.setEncoding('utf8');
47
+ res.on('data', (c) => { data += c; });
48
+ res.on('end', () => {
49
+ try {
50
+ const j = JSON.parse(data || '{}');
51
+ if (j.ok) resolve(j.result);
52
+ else reject(new Error(`telegram ${method}: ${j.description || 'hata ' + res.statusCode}`));
53
+ } catch (e) {
54
+ reject(new Error(`telegram ${method}: bozuk yanıt`));
55
+ }
56
+ });
57
+ }
58
+ );
59
+ req.on('timeout', () => req.destroy(new Error('zaman aşımı')));
60
+ req.on('error', reject);
61
+ this._req = req;
62
+ req.write(payload);
63
+ req.end();
64
+ });
65
+ }
66
+
67
+ async start() {
68
+ if (!this.token) {
69
+ this._setStatus('error');
70
+ return false;
71
+ }
72
+ this.stopping = false;
73
+ this.offset = 0;
74
+ this._setStatus('connecting');
75
+ try {
76
+ const me = await this.api('getMe', {});
77
+ this.me = me;
78
+ this._setStatus('connected', '@' + (me.username || me.first_name || 'bot'));
79
+ } catch (e) {
80
+ this._setStatus('error');
81
+ throw e;
82
+ }
83
+ this._pollLoop().catch(() => {});
84
+ return true;
85
+ }
86
+
87
+ /* long polling döngüsü — bağlantı koparsa 3 sn bekleyip devam */
88
+ async _pollLoop() {
89
+ while (!this.stopping) {
90
+ try {
91
+ const updates = await this.api(
92
+ 'getUpdates',
93
+ { timeout: 25, offset: this.offset, allowed_updates: ['message'] },
94
+ { timeout: 35000 }
95
+ );
96
+ if (this.stopping) break;
97
+ for (const u of Array.isArray(updates) ? updates : []) {
98
+ this.offset = Math.max(this.offset, (u.update_id || 0) + 1);
99
+ const msg = u.message;
100
+ if (!msg || !msg.text || !msg.from || msg.from.is_bot) continue;
101
+ const chatId = msg.chat && msg.chat.id;
102
+ if (chatId === undefined || chatId === null) continue;
103
+ const payload = {
104
+ text: String(msg.text).slice(0, 6000),
105
+ senderId: String(msg.from.id || ''),
106
+ username: String(msg.from.username || ''),
107
+ senderName: String(msg.from.first_name || msg.from.username || ''),
108
+ isGroup: !!(msg.chat && (msg.chat.type === 'group' || msg.chat.type === 'supergroup')),
109
+ };
110
+ try {
111
+ if (this.onIncoming) this.onIncoming(String(chatId), payload);
112
+ } catch {}
113
+ }
114
+ } catch (e) {
115
+ if (this.stopping) break;
116
+ this.emit({ type: 'poll-error', error: String((e && e.message) || e) });
117
+ await new Promise((r) => setTimeout(r, 3000));
118
+ }
119
+ }
120
+ }
121
+
122
+ async stop() {
123
+ this.stopping = true;
124
+ try { if (this._req) this._req.destroy(new Error('stop')); } catch {}
125
+ this._req = null;
126
+ this.connected = false;
127
+ this.status = 'disconnected';
128
+ }
129
+
130
+ snapshot() {
131
+ return {
132
+ status: this.status,
133
+ user: this.me ? '@' + (this.me.username || this.me.first_name || 'bot') : null,
134
+ connected: this.connected,
135
+ };
136
+ }
137
+
138
+ /* Metin gönder — 4096 sınırı için parçalara böl */
139
+ async send(chatId, text) {
140
+ const t = String(text || '');
141
+ if (!t.trim()) return false;
142
+ const chunks = [];
143
+ for (let i = 0; i < t.length; i += SEND_CHUNK) chunks.push(t.slice(i, i + SEND_CHUNK));
144
+ for (const part of chunks) {
145
+ await this.api('sendMessage', {
146
+ chat_id: chatId,
147
+ text: part,
148
+ disable_web_page_preview: true,
149
+ });
150
+ }
151
+ return true;
152
+ }
153
+ }
154
+
155
+ module.exports = { TelegramBridge };
@@ -1,39 +1,39 @@
1
- 'use strict';
2
-
3
- /* Beast tokens: dependency-free token estimation for context budgeting.
4
- Accuracy is calibrated at runtime with real usage.prompt_tokens values
5
- fed back from the API (see Engine.tokRatio). */
6
-
7
- function estTokens(text) {
8
- const s = String(text || '');
9
- if (!s) return 0;
10
- let narrow = 0;
11
- let wide = 0;
12
- for (let i = 0; i < s.length; i++) {
13
- // CJK ve emoji gibi geniş karakterler token başına daha fazla maliyetlidir
14
- if (s.charCodeAt(i) > 0x2e7f) wide++;
15
- else narrow++;
16
- }
17
- return Math.ceil((narrow + wide * 2.5) / 4);
18
- }
19
-
20
- function estMsgTokens(m) {
21
- if (!m) return 0;
22
- let t = 4; // mesaj zarfı (role vb.)
23
- const content = m.content;
24
- if (typeof content === 'string') {
25
- t += estTokens(content);
26
- } else if (Array.isArray(content)) {
27
- for (const p of content) {
28
- if (p && p.type === 'text') t += estTokens(p.text);
29
- else if (p && p.type === 'image_url') t += 600; // düşük detaylı görsel yaklaşımı
30
- else t += estTokens(JSON.stringify(p || ''));
31
- }
32
- }
33
- if (m.tool_calls && m.tool_calls.length) {
34
- t += estTokens(JSON.stringify(m.tool_calls)) + 3 * m.tool_calls.length;
35
- }
36
- return t;
37
- }
38
-
39
- module.exports = { estTokens, estMsgTokens };
1
+ 'use strict';
2
+
3
+ /* Beast tokens: dependency-free token estimation for context budgeting.
4
+ Accuracy is calibrated at runtime with real usage.prompt_tokens values
5
+ fed back from the API (see Engine.tokRatio). */
6
+
7
+ function estTokens(text) {
8
+ const s = String(text || '');
9
+ if (!s) return 0;
10
+ let narrow = 0;
11
+ let wide = 0;
12
+ for (let i = 0; i < s.length; i++) {
13
+ // CJK ve emoji gibi geniş karakterler token başına daha fazla maliyetlidir
14
+ if (s.charCodeAt(i) > 0x2e7f) wide++;
15
+ else narrow++;
16
+ }
17
+ return Math.ceil((narrow + wide * 2.5) / 4);
18
+ }
19
+
20
+ function estMsgTokens(m) {
21
+ if (!m) return 0;
22
+ let t = 4; // mesaj zarfı (role vb.)
23
+ const content = m.content;
24
+ if (typeof content === 'string') {
25
+ t += estTokens(content);
26
+ } else if (Array.isArray(content)) {
27
+ for (const p of content) {
28
+ if (p && p.type === 'text') t += estTokens(p.text);
29
+ else if (p && p.type === 'image_url') t += 600; // düşük detaylı görsel yaklaşımı
30
+ else t += estTokens(JSON.stringify(p || ''));
31
+ }
32
+ }
33
+ if (m.tool_calls && m.tool_calls.length) {
34
+ t += estTokens(JSON.stringify(m.tool_calls)) + 3 * m.tool_calls.length;
35
+ }
36
+ return t;
37
+ }
38
+
39
+ module.exports = { estTokens, estMsgTokens };
@@ -1,125 +1,125 @@
1
- 'use strict';
2
-
3
- /* Beast kullanım sayacı: model çağrılarının token/çağrı/maliyet toplamları.
4
- Gün bazlı tutulur (%APPDATA%\beast\usage.json), ~70 gün saklanır.
5
- Maliyet USD: config.yaml providers.price_in / price_out (1M token başına)
6
- verildiyse hesaplanır; verilmiyorsa 0 kalır (token raporu yine doğru). */
7
-
8
- const fs = require('fs');
9
- const path = require('path');
10
- const { beastRoot } = require('./memory');
11
-
12
- function file() {
13
- return path.join(beastRoot(), 'usage.json');
14
- }
15
-
16
- let days = []; // [{ date:'YYYY-MM-DD', models:{ 'pid::model': {calls,pin,pout,cost} } }]
17
- let loaded = false;
18
-
19
- function today() {
20
- const d = new Date();
21
- const p = (n) => String(n).padStart(2, '0');
22
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
23
- }
24
-
25
- function load() {
26
- if (loaded) return;
27
- loaded = true;
28
- try {
29
- const raw = JSON.parse(fs.readFileSync(file(), 'utf8'));
30
- days = Array.isArray(raw.days) ? raw.days.slice(-70) : [];
31
- } catch {
32
- days = [];
33
- }
34
- }
35
-
36
- function save() {
37
- try {
38
- fs.mkdirSync(beastRoot(), { recursive: true });
39
- const tmp = file() + '.tmp';
40
- fs.writeFileSync(tmp, JSON.stringify({ days }, null, 2));
41
- fs.renameSync(tmp, file());
42
- } catch {}
43
- }
44
-
45
- function dayRow(date) {
46
- let row = days.find((d) => d.date === date);
47
- if (!row) {
48
- row = { date, models: {} };
49
- days.push(row);
50
- if (days.length > 70) days = days.slice(-70);
51
- }
52
- return row;
53
- }
54
-
55
- function keyOf(providerId, model) {
56
- return `${providerId || '?'}::${model || '?'}`;
57
- }
58
-
59
- /* Bir çağrının kullanımını işle. meta: {providerId, model, costIn, costOut}
60
- (1M token fiyatları, opsiyonel). Saf davranış + disk. */
61
- function record({ providerId, model, promptTokens = 0, completionTokens = 0, costIn = null, costOut = null } = {}) {
62
- load();
63
- const pin = Math.max(0, Math.round(Number(promptTokens) || 0));
64
- const pout = Math.max(0, Math.round(Number(completionTokens) || 0));
65
- if (!pin && !pout) return;
66
- const row = dayRow(today());
67
- const k = keyOf(providerId, model);
68
- const m = row.models[k] || (row.models[k] = { calls: 0, pin: 0, pout: 0, cost: 0 });
69
- m.calls += 1;
70
- m.pin += pin;
71
- m.pout += pout;
72
- if (Number.isFinite(costIn) && Number.isFinite(costOut)) {
73
- m.cost += (pin * costIn + pout * costOut) / 1000000;
74
- }
75
- save();
76
- }
77
-
78
- function sumRows(rows) {
79
- const out = {};
80
- for (const r of rows) {
81
- for (const [k, v] of Object.entries(r.models || {})) {
82
- const t = out[k] || (out[k] = { calls: 0, pin: 0, pout: 0, cost: 0 });
83
- t.calls += v.calls || 0;
84
- t.pin += v.pin || 0;
85
- t.pout += v.pout || 0;
86
- t.cost += v.cost || 0;
87
- }
88
- }
89
- return out;
90
- }
91
-
92
- function monthPrefix() {
93
- return today().slice(0, 7);
94
- }
95
-
96
- function report() {
97
- load();
98
- const t = today();
99
- const mp = monthPrefix();
100
- const todayModels = sumRows(days.filter((d) => d.date === t));
101
- const monthModels = sumRows(days.filter((d) => String(d.date || '').startsWith(mp)));
102
- const flatten = (models) =>
103
- Object.entries(models)
104
- .map(([k, v]) => ({ model: k, ...v }))
105
- .sort((a, b) => b.calls - a.calls);
106
- return {
107
- today: { date: t, total: totalOf(todayModels), models: flatten(todayModels) },
108
- month: { prefix: mp, total: totalOf(monthModels), models: flatten(monthModels) },
109
- };
110
- }
111
-
112
- function totalOf(models) {
113
- let calls = 0, pin = 0, pout = 0, cost = 0;
114
- for (const v of Object.values(models)) {
115
- calls += v.calls; pin += v.pin; pout += v.pout; cost += v.cost;
116
- }
117
- return { calls, pin, pout, cost: Math.round(cost * 10000) / 10000 };
118
- }
119
-
120
- function reset() {
121
- days = [];
122
- save();
123
- }
124
-
125
- module.exports = { record, report, reset, sumRows };
1
+ 'use strict';
2
+
3
+ /* Beast kullanım sayacı: model çağrılarının token/çağrı/maliyet toplamları.
4
+ Gün bazlı tutulur (%APPDATA%\beast\usage.json), ~70 gün saklanır.
5
+ Maliyet USD: config.yaml providers.price_in / price_out (1M token başına)
6
+ verildiyse hesaplanır; verilmiyorsa 0 kalır (token raporu yine doğru). */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { beastRoot } = require('./memory');
11
+
12
+ function file() {
13
+ return path.join(beastRoot(), 'usage.json');
14
+ }
15
+
16
+ let days = []; // [{ date:'YYYY-MM-DD', models:{ 'pid::model': {calls,pin,pout,cost} } }]
17
+ let loaded = false;
18
+
19
+ function today() {
20
+ const d = new Date();
21
+ const p = (n) => String(n).padStart(2, '0');
22
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
23
+ }
24
+
25
+ function load() {
26
+ if (loaded) return;
27
+ loaded = true;
28
+ try {
29
+ const raw = JSON.parse(fs.readFileSync(file(), 'utf8'));
30
+ days = Array.isArray(raw.days) ? raw.days.slice(-70) : [];
31
+ } catch {
32
+ days = [];
33
+ }
34
+ }
35
+
36
+ function save() {
37
+ try {
38
+ fs.mkdirSync(beastRoot(), { recursive: true });
39
+ const tmp = file() + '.tmp';
40
+ fs.writeFileSync(tmp, JSON.stringify({ days }, null, 2));
41
+ fs.renameSync(tmp, file());
42
+ } catch {}
43
+ }
44
+
45
+ function dayRow(date) {
46
+ let row = days.find((d) => d.date === date);
47
+ if (!row) {
48
+ row = { date, models: {} };
49
+ days.push(row);
50
+ if (days.length > 70) days = days.slice(-70);
51
+ }
52
+ return row;
53
+ }
54
+
55
+ function keyOf(providerId, model) {
56
+ return `${providerId || '?'}::${model || '?'}`;
57
+ }
58
+
59
+ /* Bir çağrının kullanımını işle. meta: {providerId, model, costIn, costOut}
60
+ (1M token fiyatları, opsiyonel). Saf davranış + disk. */
61
+ function record({ providerId, model, promptTokens = 0, completionTokens = 0, costIn = null, costOut = null } = {}) {
62
+ load();
63
+ const pin = Math.max(0, Math.round(Number(promptTokens) || 0));
64
+ const pout = Math.max(0, Math.round(Number(completionTokens) || 0));
65
+ if (!pin && !pout) return;
66
+ const row = dayRow(today());
67
+ const k = keyOf(providerId, model);
68
+ const m = row.models[k] || (row.models[k] = { calls: 0, pin: 0, pout: 0, cost: 0 });
69
+ m.calls += 1;
70
+ m.pin += pin;
71
+ m.pout += pout;
72
+ if (Number.isFinite(costIn) && Number.isFinite(costOut)) {
73
+ m.cost += (pin * costIn + pout * costOut) / 1000000;
74
+ }
75
+ save();
76
+ }
77
+
78
+ function sumRows(rows) {
79
+ const out = {};
80
+ for (const r of rows) {
81
+ for (const [k, v] of Object.entries(r.models || {})) {
82
+ const t = out[k] || (out[k] = { calls: 0, pin: 0, pout: 0, cost: 0 });
83
+ t.calls += v.calls || 0;
84
+ t.pin += v.pin || 0;
85
+ t.pout += v.pout || 0;
86
+ t.cost += v.cost || 0;
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+
92
+ function monthPrefix() {
93
+ return today().slice(0, 7);
94
+ }
95
+
96
+ function report() {
97
+ load();
98
+ const t = today();
99
+ const mp = monthPrefix();
100
+ const todayModels = sumRows(days.filter((d) => d.date === t));
101
+ const monthModels = sumRows(days.filter((d) => String(d.date || '').startsWith(mp)));
102
+ const flatten = (models) =>
103
+ Object.entries(models)
104
+ .map(([k, v]) => ({ model: k, ...v }))
105
+ .sort((a, b) => b.calls - a.calls);
106
+ return {
107
+ today: { date: t, total: totalOf(todayModels), models: flatten(todayModels) },
108
+ month: { prefix: mp, total: totalOf(monthModels), models: flatten(monthModels) },
109
+ };
110
+ }
111
+
112
+ function totalOf(models) {
113
+ let calls = 0, pin = 0, pout = 0, cost = 0;
114
+ for (const v of Object.values(models)) {
115
+ calls += v.calls; pin += v.pin; pout += v.pout; cost += v.cost;
116
+ }
117
+ return { calls, pin, pout, cost: Math.round(cost * 10000) / 10000 };
118
+ }
119
+
120
+ function reset() {
121
+ days = [];
122
+ save();
123
+ }
124
+
125
+ module.exports = { record, report, reset, sumRows };