beast-agent 2.7.2 → 2.8.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.7.2",
4
+ "version": "2.8.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -18,6 +18,7 @@ const nightref = require('./nightref');
18
18
  const skills = require('./skills');
19
19
  const mcp = require('./mcp');
20
20
  const apps = require('./apps');
21
+ const pdfwrite = require('./pdfwrite');
21
22
  const { estTokens, estMsgTokens } = require('./tokens');
22
23
  const log = require('./logger');
23
24
 
@@ -1661,7 +1662,7 @@ class Engine {
1661
1662
  'CONUŞMA ODAĞI SENDE KALSIN: kullanıcı seninle konuşurken iş çıkmışsa — uzun da olsa UFACIK da (tek komutluk dizin listesi, tek dosya okuma, tek arama…) — run_background ile PARALEL ajana devret; ana sohbet hiçbir işi beklemez; bittiğinde özet otomatik düşer.',
1662
1663
  'Python işleri için python_run kullan: küçük betikler inline code ile; tekrarlayan işler %APPDATA%\\beast\\scripts klasöründeki dosyalarla (ör. news.py = RSS haber toplayıcı: args ["--limit","8","--json"]). Python kurulu olmasa bile ilk çağrıda taşınabilir gömülü runtime otomatik iner.',
1663
1664
  'PYTHON DURUMU: makinede sistem Python\'u görünmese bile ŞAŞIRMA ve "python yok" DEME — python_run aracı kendi taşınabilir runtime\'ını (%APPDATA%\\beast\\py\\python.exe) otomatik indirir/kullanır ve bu klasör run_command PATH\'inde önceliklidir; yani run_command içinde de `python` çalışır. Ham Google/Bing scrape yerine önce web_search aracını kullan (SearXNG + stealth TLS + TinyFish + Python çoklu-motor destekli), script gerektiğinde python_run yaz.',
1664
- 'PDF ÇIKTI KURALI: PDF üretirken pip\u2019ten pdf paketi (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) KURMA/KULLANMA — bunlar Türkçe karakterleri bozar. Doğru kit Node tarafında ZATEN kurulu: `pdf-lib` + `@pdf-lib/fontkit` (Türkçe font gömme) ve `pdfkit`. python_run ile DEĞİL; write_file ile .js script yazıp run_command ile `node script.js` çalıştır. md→pdf çevirici YOKTUR ve kurulmaz: kullanıcıya rapor/özet/belge çıktısı vereceksen .md dosyası gönderme — aynı içeriği DOĞRUDAN pdf-lib/pdfkit ile PDF olarak üret ve send_file ile o PDF\u2019i gönder. Ayrıntılı örnekler: pdf skill\u2019i (SKILL.md).',
1665
+ 'PDF ÇIKTI KURALI: PDF üretirken pip\u2019ten pdf paketi (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) KURMA/KULLANMA — bunlar Türkçe karakterleri bozar. İLK TERCİH pdf_write aracı: tek çağrıda başlık+markdown-lite içeriği düzenli, Türkçe-güvenli PDF\u2019e çevirir (Windows fontu otomatik gömülür; ğ ş ı İ bozulmaz) — script YAZMA, pip KULLANMA. Sıradışı ihtiyaç (form doldurma, PDF birleştirme, sayfa işleme) için doğru kit Node\u2019ta ZATEN kurulu: `pdf-lib` + `@pdf-lib/fontkit` ve `pdfkit` — o zaman write_file ile .js script yazıp run_command ile `node script.js` çalıştır (python_run İLE DEĞİL). md→pdf çevirici YOKTUR ve kurulmaz: kullanıcıya rapor/özet/belge çıktısı vereceksen .md dosyası gönderme — pdf_write ile DOĞRUDAN PDF üret ve send_file ile gönder. Ayrıntılı örnekler: pdf skill\u2019i (SKILL.md).',
1665
1666
  'Eski bir hafıza kaydına ihtiyacın olursa memory_search ile ara; kalıcı bilgi/birikim için kb_search kullan, yeni bilgi öğrenirsen kb_add ile kaynak belirt.',
1666
1667
  'Bir oturumda 3+ kez memory_write yaptıysan iş bitince memory_hygiene çağır (duplike/eskime temizliği).',
1667
1668
  'BEAST KAYNAK KORUMASI: Beast\u2019in kendi kurulum/kaynak kod klasörü KİLİTLİDİR — oraya dosya yazamaz, silamaz, komut/betikle değiştiremezsin; okumak serbest. Kullanıcı kodu ancak kendi eliyle dışarıdan değiştirir; böyle bir istek gelirse yapamayacağını söyle ve kullanıcının elle yapması için yol göster.',
@@ -2350,6 +2351,9 @@ class Engine {
2350
2351
  ) {
2351
2352
  lastMsg.content = (lastMsg.content + '\n\n' + body).slice(0, USER_MAX);
2352
2353
  this._rewriteLastMsg(s.id, lastMsg.content, null, 'assistant');
2354
+ /* UI'ya bildir: birleştirme yeni bubble basmaz — son asistan balonu
2355
+ güncellenir (yoksa proaktif not ekranda hiç görünmezdi) */
2356
+ this.emit({ type: 'message-merge', sessionId: s.id, message: lastMsg });
2353
2357
  this.emit({ type: 'sessions' });
2354
2358
  return true;
2355
2359
  }
@@ -4206,6 +4210,16 @@ const skills = require('./skills');
4206
4210
  const r = await this.fileSend(sessionId, p, String((args && args.caption) || ''));
4207
4211
  return JSON.stringify(r);
4208
4212
  }
4213
+ if (name === 'pdf_write') {
4214
+ const r = await pdfwrite.write({
4215
+ outPath: String((args && args.path) || ''),
4216
+ title: String((args && args.title) || ''),
4217
+ subtitle: String((args && args.subtitle) || ''),
4218
+ content: String((args && args.content) || ''),
4219
+ workspace: this.workspace,
4220
+ });
4221
+ return JSON.stringify(r);
4222
+ }
4209
4223
  if (name === 'run_background') {
4210
4224
  if (args.agent && !agentdefs.get(String(args.agent))) {
4211
4225
  return JSON.stringify({
@@ -4902,6 +4916,24 @@ const TOOLS = [
4902
4916
  },
4903
4917
  },
4904
4918
  },
4919
+ {
4920
+ type: 'function',
4921
+ function: {
4922
+ name: 'pdf_write',
4923
+ description:
4924
+ 'Create a polished, Turkish-safe PDF document in ONE call — NO script writing, NO pip packages (Turkish characters ğ ş ı İ Ğ Ş İ Ö Ü render perfectly; a Windows font is embedded automatically). content = markdown-lite: "# ## ###" headings, plain paragraphs, "- " bullets, "1. " numbered items, **bold**, `mono`, "> " quote, "```" code blocks, "| col | col |" table rows. Then call send_file with the returned path to deliver it. Use this for reports/summaries/notes/documents instead of writing a script file.',
4925
+ parameters: {
4926
+ type: 'object',
4927
+ properties: {
4928
+ path: { type: 'string', description: 'Output file path (absolute or workspace-relative; .pdf is added if missing)' },
4929
+ title: { type: 'string', description: 'Document title shown at the top (optional but recommended)' },
4930
+ subtitle: { type: 'string', description: 'Small line under the title (date, author, subject…)' },
4931
+ content: { type: 'string', description: 'Markdown-lite body: headings, paragraphs, lists, **bold**, tables, code blocks' },
4932
+ },
4933
+ required: ['path', 'content'],
4934
+ },
4935
+ },
4936
+ },
4905
4937
  {
4906
4938
  type: 'function',
4907
4939
  function: {
@@ -0,0 +1,528 @@
1
+ 'use strict';
2
+
3
+ /* Beast PDF ÜRETİCİ — tek çağrıda kusursuz, Türkçe-güvenli PDF.
4
+ pdf-lib + @pdf-lib/fontkit ile Windows TTF gömer (ğ ş ı İ Ğ Ş İ Ö Ü garantili;
5
+ Helvetica gibi standard fontlar bunları BASAMAZ). Markdown-lite ayrıştırır:
6
+ # ## ### başlıklar, paragraf, -/* bullet, 1. numaralı liste, **bold**,
7
+ `kod`, | tablo satırları |, > alıntı, ``` kod bloğu. Otomatik satır
8
+ kaydırma + sayfa geçişi + sayfa numarası + başlık bloğu.
9
+ Ajanın elle .js script yazıp font yolu/uğraşmasına GEREK YOK — pdf_write aracı bunu çağırır. */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ /* Türkçe glifleri TAM kapsayan Windows sistem fontları — sırayla denenir.
15
+ [normal, bold, mono] */
16
+ const FONT_SETS = [
17
+ ['C:/Windows/Fonts/segoeui.ttf', 'C:/Windows/Fonts/segoeuib.ttf', 'C:/Windows/Fonts/consola.ttf'],
18
+ ['C:/Windows/Fonts/arial.ttf', 'C:/Windows/Fonts/arialbd.ttf', 'C:/Windows/Fonts/consola.ttf'],
19
+ ['C:/Windows/Fonts/calibri.ttf', 'C:/Windows/Fonts/calibrib.ttf', 'C:/Windows/Fonts/consola.ttf'],
20
+ ['C:/Windows/Fonts/tahoma.ttf', 'C:/Windows/Fonts/tahomabd.ttf', 'C:/Windows/Fonts/consola.ttf'],
21
+ ];
22
+
23
+ function firstFont() {
24
+ for (const set of FONT_SETS) {
25
+ if (set.slice(0, 2).every((f) => fs.existsSync(f))) return set;
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function hasFonts() {
31
+ return !!firstFont();
32
+ }
33
+
34
+ const CONTENT_CAP = 300000;
35
+
36
+ /* ---------- markdown-lite → bloklar ---------- */
37
+
38
+ function parseBlocks(md) {
39
+ const lines = String(md || '').replace(/\r\n?/g, '\n').split('\n');
40
+ const blocks = [];
41
+ let i = 0;
42
+ while (i < lines.length) {
43
+ const raw = lines[i];
44
+ const line = raw.trim();
45
+ if (!line) {
46
+ i++;
47
+ continue;
48
+ }
49
+ if (/^```/.test(line)) {
50
+ const buf = [];
51
+ i++;
52
+ while (i < lines.length && !/^```/.test(lines[i].trim())) {
53
+ buf.push(lines[i].replace(/\t/g, ' '));
54
+ i++;
55
+ }
56
+ i++; // kapanış fence (ya da EOF)
57
+ blocks.push({ type: 'code', lines: buf });
58
+ continue;
59
+ }
60
+ let m;
61
+ if ((m = line.match(/^(#{1,3})\s+(.*)$/))) {
62
+ blocks.push({ type: 'h', level: m[1].length, text: m[2].replace(/\s*#+\s*$/, '') });
63
+ i++;
64
+ continue;
65
+ }
66
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(line)) {
67
+ blocks.push({ type: 'hr' });
68
+ i++;
69
+ continue;
70
+ }
71
+ if ((m = line.match(/^>\s?(.*)$/))) {
72
+ blocks.push({ type: 'quote', text: m[1] });
73
+ i++;
74
+ continue;
75
+ }
76
+ if ((m = line.match(/^[-*•]\s+(.*)$/))) {
77
+ blocks.push({ type: 'li', marker: '\u2022', text: m[1] });
78
+ i++;
79
+ continue;
80
+ }
81
+ if ((m = line.match(/^(\d{1,3})[.)]\s+(.*)$/))) {
82
+ blocks.push({ type: 'li', marker: m[1] + '.', text: m[2] });
83
+ i++;
84
+ continue;
85
+ }
86
+ if (line.startsWith('|')) {
87
+ const rows = [];
88
+ while (i < lines.length && lines[i].trim().startsWith('|')) {
89
+ const cells = lines[i]
90
+ .trim()
91
+ .replace(/^\|/, '')
92
+ .replace(/\|$/, '')
93
+ .split('|')
94
+ .map((c) => c.trim());
95
+ if (!cells.every((c) => /^:?-{2,}:?$/.test(c) || c === '')) rows.push(cells);
96
+ i++;
97
+ }
98
+ if (rows.length) blocks.push({ type: 'table', rows });
99
+ continue;
100
+ }
101
+ /* paragraf: özel blok başlatmayan ardışık satırlar birleşir */
102
+ const para = [line];
103
+ i++;
104
+ while (i < lines.length) {
105
+ const l2 = lines[i].trim();
106
+ if (
107
+ !l2 ||
108
+ /^(#{1,3}\s|[-*•]\s|\d{1,3}[.)]\s|>|```|\|)/.test(l2) ||
109
+ /^(-{3,}|\*{3,}|_{3,})$/.test(l2)
110
+ )
111
+ break;
112
+ para.push(l2);
113
+ i++;
114
+ }
115
+ blocks.push({ type: 'p', text: para.join(' ') });
116
+ }
117
+ return blocks;
118
+ }
119
+
120
+ /* ---------- inline: **bold** ve `mono` → span dizisi ---------- */
121
+
122
+ function toSpans(text) {
123
+ const spans = [];
124
+ const re = /(\*\*([^*]+)\*\*)|(`([^`]+)`)/g;
125
+ let last = 0;
126
+ let m;
127
+ while ((m = re.exec(text))) {
128
+ if (m.index > last) spans.push({ t: text.slice(last, m.index), b: false });
129
+ if (m[1]) spans.push({ t: m[2], b: true });
130
+ else spans.push({ t: m[4], b: false, mono: true });
131
+ last = re.lastIndex;
132
+ }
133
+ if (last < text.length) spans.push({ t: text.slice(last), b: false });
134
+ return spans.length ? spans : [{ t: '', b: false }];
135
+ }
136
+
137
+ /* ---------- satır kaydırma: span'ler → [token] satırları ---------- */
138
+
139
+ function wrapSpans(spans, fontFor, size, maxW) {
140
+ const tokens = [];
141
+ for (const s of spans) {
142
+ const parts = s.t.split(/(\s+)/);
143
+ for (const w of parts) {
144
+ if (!w) continue;
145
+ tokens.push({ w, b: !!s.b, mono: !!s.mono, space: /^\s+$/.test(w) });
146
+ }
147
+ }
148
+ const widthOf = (tk) => {
149
+ try {
150
+ return fontFor(tk).widthOfTextAtSize(tk.w, size);
151
+ } catch {
152
+ return tk.w.length * size * 0.6;
153
+ }
154
+ };
155
+ const lines = [];
156
+ let cur = [];
157
+ let curW = 0;
158
+ const flush = () => {
159
+ while (cur.length && cur[cur.length - 1].space) cur.pop();
160
+ if (cur.length) lines.push(cur);
161
+ cur = [];
162
+ curW = 0;
163
+ };
164
+ for (const tk of tokens) {
165
+ if (tk.space) {
166
+ if (cur.length) {
167
+ cur.push(tk);
168
+ curW += widthOf(tk);
169
+ }
170
+ continue;
171
+ }
172
+ let w = widthOf(tk);
173
+ /* tek kelime satırdan genişse karakter karakter böl (uzun URL/link emniyeti) */
174
+ if (w > maxW) {
175
+ flush();
176
+ let chunk = '';
177
+ let chunkW = 0;
178
+ for (const ch of tk.w) {
179
+ const cw = (() => {
180
+ try {
181
+ return fontFor(tk).widthOfTextAtSize(ch, size);
182
+ } catch {
183
+ return size * 0.6;
184
+ }
185
+ })();
186
+ if (chunk && chunkW + cw > maxW) {
187
+ lines.push([{ w: chunk, b: tk.b, mono: tk.mono, space: false, hard: true }]);
188
+ chunk = ch;
189
+ chunkW = cw;
190
+ } else {
191
+ chunk += ch;
192
+ chunkW += cw;
193
+ }
194
+ }
195
+ if (chunk) {
196
+ cur = [{ w: chunk, b: tk.b, mono: tk.mono, space: false }];
197
+ curW = chunkW;
198
+ }
199
+ continue;
200
+ }
201
+ if (curW + w > maxW) flush();
202
+ cur.push(tk);
203
+ curW += w;
204
+ }
205
+ flush();
206
+ return lines;
207
+ }
208
+
209
+ /* drawTextSpan'lı satır: token dizisini çizer, son x döner */
210
+ function drawTokenLine(page, line, x, y, fontFor, size, color) {
211
+ let cx = x;
212
+ for (const tk of line) {
213
+ if (tk.space) {
214
+ try {
215
+ cx += fontFor(tk).widthOfTextAtSize(tk.w, size);
216
+ } catch {
217
+ cx += tk.w.length * size * 0.6;
218
+ }
219
+ continue;
220
+ }
221
+ try {
222
+ page.drawText(tk.w, { x: cx, y, size, font: fontFor(tk), color });
223
+ cx += fontFor(tk).widthOfTextAtSize(tk.w, size);
224
+ } catch {
225
+ cx += tk.w.length * size * 0.6;
226
+ }
227
+ }
228
+ return cx;
229
+ }
230
+
231
+ function lineCount(lines) {
232
+ return Math.max(1, lines.length);
233
+ }
234
+
235
+ /* ---------- ana üretici ---------- */
236
+
237
+ async function writePdf({ outPath, title, subtitle, content, workspace }) {
238
+ try {
239
+ const raw = String((content == null ? '' : content) || '');
240
+ if (!raw.trim()) return { ok: false, error: 'content gerekli' };
241
+ const outRaw = String(outPath || '').trim();
242
+ if (!outRaw) return { ok: false, error: 'path gerekli' };
243
+ let abs = path.isAbsolute(outRaw) ? outRaw : path.join(String(workspace || process.cwd()), outRaw);
244
+ if (!/\.pdf$/i.test(abs)) abs += '.pdf';
245
+
246
+ const fonts = firstFont();
247
+ if (!fonts) {
248
+ return {
249
+ ok: false,
250
+ error:
251
+ 'Türkçe destekli Windows TTF bulunamadı (Segoe UI/Arial/Calibri/Tahoma) — pdf skill\u2019indeki elle şablonu kullan',
252
+ };
253
+ }
254
+
255
+ const { PDFDocument, rgb } = require('pdf-lib');
256
+ const fontkit = require('@pdf-lib/fontkit');
257
+ const doc = await PDFDocument.create();
258
+ doc.registerFontkit(fontkit);
259
+ const [fReg, fBold, fMono] = fonts;
260
+ const reg = await doc.embedFont(fs.readFileSync(fReg), { subset: true });
261
+ let bold = reg;
262
+ try {
263
+ if (fs.existsSync(fBold)) bold = await doc.embedFont(fs.readFileSync(fBold), { subset: true });
264
+ } catch {}
265
+ let mono = reg;
266
+ try {
267
+ if (fMono && fs.existsSync(fMono)) mono = await doc.embedFont(fs.readFileSync(fMono), { subset: true });
268
+ } catch {}
269
+
270
+ const PAGE_W = 595.28;
271
+ const PAGE_H = 841.89;
272
+ const MX = 56;
273
+ const MT = 64;
274
+ const MB = 76;
275
+ const availW = PAGE_W - 2 * MX;
276
+ const textC = rgb(0.13, 0.13, 0.16);
277
+ const mutedC = rgb(0.42, 0.45, 0.5);
278
+ const accentC = rgb(0.15, 0.35, 0.6);
279
+ const codeBgC = rgb(0.95, 0.96, 0.97);
280
+ const rowBgC = rgb(0.96, 0.96, 0.98);
281
+
282
+ let page = doc.addPage([PAGE_W, PAGE_H]);
283
+ let y = PAGE_H - MT;
284
+
285
+ const newPage = () => {
286
+ page = doc.addPage([PAGE_W, PAGE_H]);
287
+ y = PAGE_H - MT;
288
+ };
289
+ const ensure = (h) => {
290
+ if (y - h < MB) newPage();
291
+ };
292
+
293
+ const fontFor = (tk) => (tk.mono ? mono : tk.b ? bold : reg);
294
+ const H_SIZES = { 1: 20, 2: 15.5, 3: 13 };
295
+
296
+ /* başlık bloğu */
297
+ const titleText = String(title || '').trim();
298
+ if (titleText) {
299
+ const tSpans = [{ t: titleText, b: true }];
300
+ const tLines = wrapSpans(tSpans, fontFor, 22, availW);
301
+ for (const ln of tLines) {
302
+ ensure(30);
303
+ drawTokenLine(page, ln, MX, y - 22, fontFor, 22, textC);
304
+ y -= 30;
305
+ }
306
+ const sub = String(subtitle || '').trim();
307
+ if (sub) {
308
+ const sLines = wrapSpans(toSpans(sub), fontFor, 10.5, availW);
309
+ for (const ln of sLines) {
310
+ ensure(16);
311
+ drawTokenLine(page, ln, MX, y - 10, fontFor, 10.5, mutedC);
312
+ y -= 16;
313
+ }
314
+ }
315
+ y -= 6;
316
+ page.drawLine({
317
+ start: { x: MX, y },
318
+ end: { x: PAGE_W - MX, y },
319
+ thickness: 1,
320
+ color: accentC,
321
+ });
322
+ y -= 22;
323
+ }
324
+
325
+ const blocks = parseBlocks(raw.slice(0, CONTENT_CAP));
326
+
327
+ /* title verildiyse içeriğin başındaki AYNI H1'i at (çift başlık olmasın) */
328
+ if (titleText && blocks.length && blocks[0].type === 'h' && blocks[0].level === 1) {
329
+ const norm = (s) =>
330
+ String(s || '')
331
+ .toLocaleLowerCase('tr')
332
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
333
+ .trim();
334
+ if (norm(blocks[0].text) === norm(titleText)) blocks.shift();
335
+ }
336
+
337
+ const BODY = 10.5;
338
+ const LINE = BODY * 1.42;
339
+
340
+ for (const b of blocks) {
341
+ if (b.type === 'h') {
342
+ const size = H_SIZES[b.level] || 13;
343
+ const above = b.level === 1 ? 16 : b.level === 2 ? 13 : 10;
344
+ const below = b.level === 1 ? 10 : 6;
345
+ y -= above;
346
+ ensure(size * 1.4 + below);
347
+ const lines = wrapSpans(toSpans(b.text), fontFor, size, availW);
348
+ for (const ln of lines) {
349
+ ensure(size * 1.45);
350
+ drawTokenLine(page, ln, MX, y - size, fontFor, size, textC);
351
+ y -= size * 1.45;
352
+ }
353
+ if (b.level === 1) {
354
+ y -= 4;
355
+ page.drawLine({
356
+ start: { x: MX, y },
357
+ end: { x: MX + 42, y },
358
+ thickness: 1.4,
359
+ color: accentC,
360
+ });
361
+ }
362
+ y -= below;
363
+ } else if (b.type === 'p') {
364
+ const lines = wrapSpans(toSpans(b.text), fontFor, BODY, availW);
365
+ for (const ln of lines) {
366
+ ensure(LINE);
367
+ drawTokenLine(page, ln, MX, y - BODY, fontFor, BODY, textC);
368
+ y -= LINE;
369
+ }
370
+ y -= LINE * 0.55;
371
+ } else if (b.type === 'li') {
372
+ const lines = wrapSpans(toSpans(b.text), fontFor, BODY, availW - 24);
373
+ const mW = (() => {
374
+ try {
375
+ return bold.widthOfTextAtSize(b.marker, BODY);
376
+ } catch {
377
+ return 12;
378
+ }
379
+ })();
380
+ lines.forEach((ln, idx) => {
381
+ ensure(LINE);
382
+ if (idx === 0) {
383
+ try {
384
+ page.drawText(b.marker, { x: MX + 2, y: y - BODY, size: BODY, font: bold, color: accentC });
385
+ } catch {}
386
+ }
387
+ drawTokenLine(page, ln, MX + 24, y - BODY, fontFor, BODY, textC);
388
+ y -= LINE;
389
+ });
390
+ y -= LINE * 0.28;
391
+ } else if (b.type === 'quote') {
392
+ const lines = wrapSpans(toSpans(b.text), fontFor, BODY, availW - 18);
393
+ y -= 3;
394
+ ensure(LINE * lineCount(lines) + 6);
395
+ const top = y + LINE * 0.2;
396
+ page.drawLine({
397
+ start: { x: MX + 4, y: top - LINE * lineCount(lines) },
398
+ end: { x: MX + 4, y: top },
399
+ thickness: 2,
400
+ color: accentC,
401
+ });
402
+ for (const ln of lines) {
403
+ drawTokenLine(page, ln, MX + 18, y - BODY, fontFor, BODY, mutedC);
404
+ y -= LINE;
405
+ }
406
+ y -= LINE * 0.55;
407
+ } else if (b.type === 'code') {
408
+ const CS = 9.5;
409
+ const CL = CS * 1.4;
410
+ /* kod satırları: mono genişliğine göre SERT böl (wrapSpans değil —
411
+ boşluk korunsun, sekme 4 boşluk) */
412
+ b._wrapped = b.lines
413
+ .map((l) => {
414
+ const out = [];
415
+ let cur = l;
416
+ while (cur.length) {
417
+ let cut = cur.length;
418
+ try {
419
+ while (cut > 0 && mono.widthOfTextAtSize(cur.slice(0, cut), CS) > availW - 24) cut = Math.floor(cut * 0.92);
420
+ } catch {
421
+ cut = Math.min(cur.length, 90);
422
+ }
423
+ if (cut <= 0) cut = 1;
424
+ out.push(cur.slice(0, cut));
425
+ cur = cur.slice(cut);
426
+ }
427
+ return out.length ? out : [''];
428
+ })
429
+ .flat();
430
+ const nLines = Math.max(1, b._wrapped.length);
431
+ ensure(CL * nLines + 14);
432
+ const top = y + 8;
433
+ const boxH = CL * nLines + 10;
434
+ page.drawRectangle({
435
+ x: MX - 4,
436
+ y: top - boxH,
437
+ width: availW + 8,
438
+ height: boxH,
439
+ color: codeBgC,
440
+ });
441
+ for (const cl of b._wrapped) {
442
+ try {
443
+ page.drawText(cl, { x: MX + 6, y: y - CS, size: CS, font: mono, color: textC });
444
+ } catch {}
445
+ y -= CL;
446
+ }
447
+ y -= 10;
448
+ } else if (b.type === 'hr') {
449
+ ensure(18);
450
+ y -= 8;
451
+ page.drawLine({
452
+ start: { x: MX, y },
453
+ end: { x: PAGE_W - MX, y },
454
+ thickness: 0.7,
455
+ color: rgb(0.85, 0.86, 0.9),
456
+ });
457
+ y -= 14;
458
+ } else if (b.type === 'table') {
459
+ const TS = 9.5;
460
+ const TL = TS * 1.38;
461
+ const rows = b.rows;
462
+ const nCol = Math.max(...rows.map((r) => r.length));
463
+ if (nCol <= 0 || nCol > 12) continue;
464
+ const gap = 6;
465
+ const colW = (availW - gap * (nCol - 1)) / nCol;
466
+ const cellLines = (text) => wrapSpans(toSpans(String(text || '')), fontFor, TS, colW - 8);
467
+ const rendered = rows.map((r) => {
468
+ const cells = [];
469
+ for (let c = 0; c < nCol; c++) cells.push(cellLines(r[c] || ''));
470
+ return cells;
471
+ });
472
+ for (let r = 0; r < rendered.length; r++) {
473
+ const isHead = r === 0;
474
+ const nLines = Math.max(...rendered[r].map((cl) => cl.length), 1);
475
+ const rowH = TL * nLines + 8;
476
+ ensure(rowH + 2);
477
+ if (isHead) {
478
+ page.drawRectangle({
479
+ x: MX - 4,
480
+ y: y - rowH + 4,
481
+ width: availW + 8,
482
+ height: rowH,
483
+ color: rowBgC,
484
+ });
485
+ }
486
+ for (let c = 0; c < nCol; c++) {
487
+ let cy = y;
488
+ for (const ln of rendered[r][c]) {
489
+ const spans = isHead ? ln.map((t) => ({ ...t, b: true })) : ln;
490
+ drawTokenLine(page, spans, MX + c * (colW + gap) + 4, cy - TS, fontFor, TS, textC);
491
+ cy -= TL;
492
+ }
493
+ }
494
+ y -= rowH;
495
+ }
496
+ y -= 10;
497
+ }
498
+ }
499
+
500
+ /* sayfa numaraları: toplam sayı üretilince ikinci geçişte basılır */
501
+ const docTitle = String(title || '').slice(0, 60);
502
+ const pages = doc.getPages();
503
+ pages.forEach((pg, idx) => {
504
+ const label = `Sayfa ${idx + 1} / ${pages.length}`;
505
+ try {
506
+ const w = reg.widthOfTextAtSize(label, 8);
507
+ pg.drawText(label, { x: PAGE_W - MX - w, y: 30, size: 8, font: reg, color: mutedC });
508
+ if (docTitle) pg.drawText(docTitle, { x: MX, y: 30, size: 8, font: reg, color: mutedC });
509
+ } catch {}
510
+ });
511
+
512
+ const bytes = await doc.save();
513
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
514
+ fs.writeFileSync(abs, Buffer.from(bytes));
515
+ return {
516
+ ok: true,
517
+ path: abs,
518
+ name: path.basename(abs),
519
+ pages: pages.length,
520
+ bytes: bytes.length,
521
+ note: 'PDF üretildi (Türkçe font gömülü) — kullanıcıya ulaşmak için send_file çağır',
522
+ };
523
+ } catch (e) {
524
+ return { ok: false, error: String((e && e.message) || e) };
525
+ }
526
+ }
527
+
528
+ module.exports = { writePdf, write: writePdf, parseBlocks, toSpans, firstFont, hasFonts };
@@ -299,25 +299,38 @@ Zorunlu kurallar:
299
299
  force: true,
300
300
  body: `---
301
301
  name: pdf
302
- description: PDF işleme rehberi — metin çıkarma (pdf-parse), Türkçe karakterli PDF oluşturma/değiştirme (pdf-lib/pdfkit), sayfa→görsel (pdf-to-img) ve taranmış PDF için OCR (tesseract.js). Kullanıcı bir .pdf dosyasından bahsederse ÖNCE bu skill'i oku.
303
- version: 1.1.0
302
+ description: PDF işleme rehberi — RAPOR/BELGE ÜRETİMİ pdf_write aracıyla TEK ÇAĞRIDA (Türkçe garantili); metin çıkarma (pdf-parse), PDF değiştirme/birleştirme (pdf-lib), sayfa→görsel (pdf-to-img), taranmış PDF için OCR (tesseract.js). Kullanıcı bir .pdf dosyasından bahsederse YA DA PDF çıktısı istenirse ÖNCE bu skill'i oku.
303
+ version: 1.2.0
304
304
  ---
305
305
 
306
306
  # PDF İşleme Rehberi
307
307
 
308
- ## EN KRİTİK KURALLAR (önce bunlar)
308
+ ## EN HIZLI YOL — pdf_write aracı (RAPOR/ÖZET/NOT/BELGE İSTEĞİNDE ÖNCE BU)
309
+
310
+ Elle script YAZMA. Ajanın pdf_write aracı tek çağrıda markdown-lite içeriği düzenli, Türkçe-güvenli PDF'e çevirir (Windows fontu otomatik gömülür — ğ ş ı İ Ğ Ş Ö Ü bozulmaz; başlık, madde işareti, tablo, sayfa numarası otomatik):
311
+
312
+ \`\`\`
313
+ pdf_write {"path": "rapor.pdf", "title": "2026 Özet Raporu", "subtitle": "07.09.2026", "content": "# Başlık\\n## Alt Başlık\\n- madde 1\\n- **önemli** madde\\n1. sıralı adım\\n| Kolon | Değer |\\n|---|---|\\n| Gelir | 12.500 TL |\\n> alıntı satırı"}
314
+ \`\`\`
315
+
316
+ Desteklenen markdown-lite: "# ## ###" başlıklar · düz paragraf · "- " bullet · "1. " numaralı · **kalın** · kod (geri tırnak içi) · "> " alıntı · kod bloğu (üç ters tırnakla açılıp kapanır) · "| kolon |" tablo satırları.
317
+
318
+ Sonra çıktıyı kullanıcıya ulaştırmak için send_file çağır. Script yazma, pip kurma — GEREK YOK.
319
+
320
+ ## EN KRİTİK KURALLAR (elle script gerektiğinde bile geçerli)
309
321
 
310
322
  1. Türkçe karakterli PDF için pip\u2019ten paket KURMA/KULLANMA (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) — bunlar ğ ş ı İ karakterlerini bozar. Doğru kit Node\u2019ta ZATEN kurulu: \`pdf-lib\` + \`@pdf-lib/fontkit\` ve \`pdfkit\`. Script\u2019i write_file ile .js yaz, run_command ile \`node script.js\` çalıştır (python_run İLE DEĞİL).
311
- 2. md→pdf çevirici YOKTUR ve kurulmaz — kullanıcıya rapor/özet/belge çıktısı vereceksen .md gönderme; aynı içeriği doğrudan PDF olarak ÜRET ve o dosyayı send_file ile gönder.
323
+ 2. md→pdf çevirici YOKTUR ve kurulmaz — kullanıcıya rapor/özet/belge çıktısı vereceksen .md gönderme; pdf_write (ya da elle pdf-lib script) ile DOĞRUDAN PDF üret ve send_file ile gönder.
312
324
  3. \`pdf-parse\` ile \`pdf-to-img\` aynı process\u2019e yüklenmez (aşağıdaki çakışma kuralı).
313
325
 
314
326
  Kurulu paketler ve rolleri:
315
327
 
316
328
  | İstek | Paket |
317
329
  |---|---|
330
+ | Rapor/özet/belge üretimi | **pdf_write aracı (tek çağrı)** |
318
331
  | Metin çıkar / oku | \`pdf-parse\` |
319
332
  | Yeni PDF üret / mevcut PDF'i değiştir | \`pdf-lib\` (+ \`@pdf-lib/fontkit\`) |
320
- | Düzenli belge üretimi (rapor, özet) | \`pdfkit\` |
333
+ | Düzenli belge üretimi (elle) | \`pdfkit\` |
321
334
  | Sayfa→PNG/JPG (taranmış/görsel PDF) | \`pdf-to-img\` |
322
335
  | Taranmış PDF'den metin (OCR) | \`pdf-to-img\` + \`tesseract.js\` |
323
336
 
package/src/main.js CHANGED
@@ -2054,8 +2054,8 @@ function fmtPerm(p) {
2054
2054
  function botToolSet(cfg) {
2055
2055
  const s = cfg && cfg.skills ? cfg.skills : {};
2056
2056
  const set = new Set([
2057
- /* çekirdek: her botta konuşma + plan + hatırlatıcı */
2058
- 'todo_write', 'set_reminder', 'send_file',
2057
+ /* çekirdek: her botta konuşma + plan + hatırlatıcı + PDF üretimi */
2058
+ 'todo_write', 'set_reminder', 'send_file', 'pdf_write',
2059
2059
  'tasks_list', 'task_status', 'task_cancel',
2060
2060
  'run_background', 'run_background_many', 'delegate_task',
2061
2061
  'event_list', 'event_subscribe', 'event_unsubscribe',
@@ -3000,6 +3000,7 @@ function reloadBackend() {
3000
3000
  emit: (ev) => {
3001
3001
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
3002
3002
  try { empatiRememberFromEvent(ev); } catch {} /* empati hafızası: sohbet → kayıt + ilgi öğrenme */
3003
+ empatiFlushInjectsOnDone(ev); /* tur ortasında kuyruğa düşen proaktif bildirimleri iş bitince geçmişe bas */
3003
3004
  flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
3004
3005
  bcFlushOnDone(ev); /* Beast Code kuyruğunu iş bitiminde boşalt */
3005
3006
  stFlushOnDone(ev); /* Beast Studio kuyruğunu iş bitiminde boşalt */
@@ -6472,11 +6473,51 @@ function empatiSourceLineWa(ev) {
6472
6473
  return title ? '🔗 ' + cap(title, 80) : '';
6473
6474
  }
6474
6475
 
6476
+ /* Enjeksiyon kuyruğu: oturum TUR ORTASINDAYKEN (ajan bir işle uğraşırken)
6477
+ gelen proaktif bildirimi sessizce DÜŞÜRME — eskisi tam olarak buydu:
6478
+ "bazen ajan konudan haber olmuyor". Metin kuyrukta bekler, oturumun
6479
+ işi bitince ('done'/'error') geçmişe asistan mesajı olarak yazılır;
6480
+ model bir sonraki turda "ben bu bildirimi attım" der, kullanıcı sorusuna
6481
+ hazırdır. */
6482
+ const empatiInjectQueue = new Map(); // sid -> [text]
6483
+ const EMPATI_INJECT_QUEUE_CAP = 20;
6484
+
6475
6485
  function empatiInjectToSession(sid, out) {
6476
- try {
6477
- if (!sid || engine.isBusy(sid)) return; /* tur ortasında geçmişi karıştırma — bildirim kanala zaten gitti */
6478
- engine.injectAssistant(sid, out);
6479
- } catch {}
6486
+ const target = String(sid || '');
6487
+ const body = String(out || '');
6488
+ if (!target || !body.trim()) return;
6489
+ let busy = false;
6490
+ try { busy = engine.isBusy(target); } catch {}
6491
+ if (busy) {
6492
+ let q = empatiInjectQueue.get(target);
6493
+ if (!q) {
6494
+ q = [];
6495
+ empatiInjectQueue.set(target, q);
6496
+ }
6497
+ if (q.length < EMPATI_INJECT_QUEUE_CAP) {
6498
+ q.push(body);
6499
+ empatiLog(`bildirim enjeksiyonu kuyrukta (ajan tur sürüyor) sid=${target} bekleyen=${q.length}`);
6500
+ }
6501
+ return;
6502
+ }
6503
+ try { engine.injectAssistant(target, body); } catch {}
6504
+ }
6505
+
6506
+ /* iş bitince (done/error) biriken proaktif enjeksiyonları geçmişe bas.
6507
+ 'done' eventi isBusy temizlenmeden ÖNCE gelir (engine.js: ctrl delete
6508
+ finally bloğunda) — flushDesktopOnDone gibi macrotask'a ertele. */
6509
+ function empatiFlushInjectsOnDone(ev) {
6510
+ if (!ev || !ev.sessionId || (ev.type !== 'done' && ev.type !== 'error')) return;
6511
+ const q = empatiInjectQueue.get(String(ev.sessionId));
6512
+ if (!q || !q.length) return;
6513
+ empatiInjectQueue.delete(String(ev.sessionId));
6514
+ setTimeout(() => {
6515
+ /* empatiInjectToSession üzerinden: arada yeni tur başladıysa yeniden kuyruğa düşer */
6516
+ for (const text of q) {
6517
+ try { empatiInjectToSession(String(ev.sessionId), text); } catch {}
6518
+ }
6519
+ empatiLog(`kuyrukta bekleyen ${q.length} proaktif bildirim geçmişe işlendi sid=${ev.sessionId}`);
6520
+ }, 250);
6480
6521
  }
6481
6522
 
6482
6523
  /* kanala giden metin + Beast'in İÇ bağlamı ayrıdır: kullanıcı linki silse bile
@@ -6635,12 +6676,16 @@ function empatiNotify(text, ev) {
6635
6676
  for (const fn of senders) {
6636
6677
  try { fn(); sent++; } catch {}
6637
6678
  }
6638
- /* hiçbir entegrasyona yazılamadıysa yalnız masaüstü chat UI'a düş —
6639
- metin güncel sohbete de asistan mesajı olarak işlenir */
6679
+ /* MASAÜSTÜ CHAT UI: entegrasyon kullanılsa da kullanılmasa da proaktif mesaj
6680
+ sohbette MESAJ olarak görünür (asistan balonu) + toast çıkar. Ajan da
6681
+ desktop oturumu geçmişinde attığı bildirimi GÖRÜR — hangi kanala gittiğine
6682
+ bakmaksızın konudan haberdar olur. Kanal oturumlarına ise BAĞLAM'lı `inject`
6683
+ gider (kanal kullanıcısı ham BAĞLAM bloğunu görmez, ajan görür). */
6640
6684
  try {
6641
- if (!sent && win && !win.isDestroyed()) {
6642
- empatiInjectToSession(empatiDesktopSid(), inject);
6643
- win.webContents.send('agent:event', { type: 'proactive', id: ev.id, level: ev.level, title: ev.title, text: out });
6685
+ if (win && !win.isDestroyed()) {
6686
+ const dsid = empatiDesktopSid();
6687
+ empatiInjectToSession(dsid, out);
6688
+ win.webContents.send('agent:event', { type: 'proactive', sessionId: dsid, id: ev.id, level: ev.level, title: ev.title, text: out });
6644
6689
  }
6645
6690
  } catch {}
6646
6691
  }
@@ -5019,6 +5019,19 @@ function onEvent(ev) {
5019
5019
  if (!hasTools) closeChatToolGroup();
5020
5020
  }
5021
5021
  break;
5022
+ case 'message-merge':
5023
+ /* proaktif not son asistan mesajına BİRLEŞTİRİLDİ (injectAssistant) —
5024
+ yeni bubble basma; son asistan balonunun içeriğini güncelle */
5025
+ {
5026
+ const bubbles = els.msgs.querySelectorAll('.msg-assistant .md');
5027
+ const el = bubbles[bubbles.length - 1];
5028
+ const txt = String((ev.message && ev.message.content) || '');
5029
+ if (el && txt) {
5030
+ el.innerHTML = md(txt);
5031
+ scrollDown();
5032
+ }
5033
+ }
5034
+ break;
5022
5035
  case 'token':
5023
5036
  ensureStreamBubble();
5024
5037
  streamRaw += ev.delta;