beast-agent 2.7.3 → 2.8.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.7.3",
4
+ "version": "2.8.1",
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.',
@@ -4209,6 +4210,16 @@ const skills = require('./skills');
4209
4210
  const r = await this.fileSend(sessionId, p, String((args && args.caption) || ''));
4210
4211
  return JSON.stringify(r);
4211
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
+ }
4212
4223
  if (name === 'run_background') {
4213
4224
  if (args.agent && !agentdefs.get(String(args.agent))) {
4214
4225
  return JSON.stringify({
@@ -4905,6 +4916,24 @@ const TOOLS = [
4905
4916
  },
4906
4917
  },
4907
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
+ },
4908
4937
  {
4909
4938
  type: 'function',
4910
4939
  function: {
package/src/agent/llm.js CHANGED
@@ -10,6 +10,7 @@
10
10
  prompt_cache_key) 400'den otomatik öğrenir → model-caps.json'a kalıcı. */
11
11
 
12
12
  const fs = require('fs');
13
+ const crypto = require('crypto');
13
14
 
14
15
  const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
15
16
  const MAX_RETRIES = 3; /* sağlayıcı geçici HTTP hataları */
@@ -328,6 +329,24 @@ async function withRetries(fn, { signal, onRetry } = {}) {
328
329
  }
329
330
  }
330
331
 
332
+ /* x-opencode-session: konuşma-başına sabit oturum kimliği — TÜM sağlayıcılara
333
+ standart gönderilir (opencode bununla isteği optimize eder; 09/06'dan
334
+ itibaren headersız istek hata dönebilir). Gerçek sohbetlerde cacheKey =
335
+ session.id; cacheKey'siz yardımcı isteklerde süreç-başına sabit yedek. */
336
+ let _sessionFallback = null;
337
+ function sessionHeaderId(body) {
338
+ const ck = String((body && body.cacheKey) || '').trim();
339
+ if (ck) return ck;
340
+ if (!_sessionFallback) {
341
+ try {
342
+ _sessionFallback = crypto.randomUUID();
343
+ } catch {
344
+ _sessionFallback = 'beast-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
345
+ }
346
+ }
347
+ return _sessionFallback;
348
+ }
349
+
331
350
  async function openChat(sel, body, { stream, signal, drops } = {}) {
332
351
  const d = drops || {};
333
352
  const payload = {
@@ -357,6 +376,7 @@ async function openChat(sel, body, { stream, signal, drops } = {}) {
357
376
  Authorization: `Bearer ${sel.key}`,
358
377
  'HTTP-Referer': 'https://localhost/beast',
359
378
  'X-Title': 'Beast Agent',
379
+ 'x-opencode-session': sessionHeaderId(body),
360
380
  },
361
381
  body: JSON.stringify(payload),
362
382
  });
@@ -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
@@ -6,6 +6,10 @@ const fs = require('fs');
6
6
  const http = require('http');
7
7
  const dns = require('dns');
8
8
  const crypto = require('crypto');
9
+ /* x-opencode-session: opencode (Zen/Go) isteklerinde beklenen oturum kimliği —
10
+ kurulum/probe/model-liste istekleri için süreç-başına sabit değer; sohbet
11
+ isteklerindeki konuşma-başına kimlik agent/llm.js'te üretilir */
12
+ const OPENCODE_SESSION = 'beast-' + crypto.randomUUID();
9
13
  const { spawn } = require('child_process');
10
14
  const { Engine, OBSERVE_MARK } = require('./agent/engine');
11
15
  const { loadBeastConfig, parseEnvFile, beastDir } = require('./agent/config');
@@ -2054,8 +2058,8 @@ function fmtPerm(p) {
2054
2058
  function botToolSet(cfg) {
2055
2059
  const s = cfg && cfg.skills ? cfg.skills : {};
2056
2060
  const set = new Set([
2057
- /* çekirdek: her botta konuşma + plan + hatırlatıcı */
2058
- 'todo_write', 'set_reminder', 'send_file',
2061
+ /* çekirdek: her botta konuşma + plan + hatırlatıcı + PDF üretimi */
2062
+ 'todo_write', 'set_reminder', 'send_file', 'pdf_write',
2059
2063
  'tasks_list', 'task_status', 'task_cancel',
2060
2064
  'run_background', 'run_background_many', 'delegate_task',
2061
2065
  'event_list', 'event_subscribe', 'event_unsubscribe',
@@ -8387,7 +8391,7 @@ async function refreshModelsAll() {
8387
8391
  try {
8388
8392
  const b = String(p.baseUrl).trim().replace(/\/+$/, '');
8389
8393
  const url = /\/v\d+$/.test(b) ? b + '/models' : b + '/v1/models';
8390
- const h = { Authorization: 'Bearer ' + p.key };
8394
+ const h = { Authorization: 'Bearer ' + p.key, 'x-opencode-session': OPENCODE_SESSION };
8391
8395
  try {
8392
8396
  if (new URL(url).hostname === 'api.anthropic.com') {
8393
8397
  h['x-api-key'] = p.key;
@@ -8484,7 +8488,10 @@ function findZenToken() {
8484
8488
 
8485
8489
  async function fetchZenFreeModels() {
8486
8490
  try {
8487
- const res = await fetch(ZEN_BASE + '/models', { signal: AbortSignal.timeout(15000) });
8491
+ const res = await fetch(ZEN_BASE + '/models', {
8492
+ headers: { 'x-opencode-session': OPENCODE_SESSION },
8493
+ signal: AbortSignal.timeout(15000),
8494
+ });
8488
8495
  if (res.ok) {
8489
8496
  const j = await res.json();
8490
8497
  const ids = (j.data || []).map((m) => (typeof m === 'string' ? m : m.id)).filter(Boolean);
@@ -8584,7 +8591,11 @@ async function testZenModel(key, model) {
8584
8591
  try {
8585
8592
  const res = await fetch(ZEN_BASE + '/chat/completions', {
8586
8593
  method: 'POST',
8587
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + key },
8594
+ headers: {
8595
+ 'Content-Type': 'application/json',
8596
+ Authorization: 'Bearer ' + key,
8597
+ 'x-opencode-session': OPENCODE_SESSION,
8598
+ },
8588
8599
  body: JSON.stringify({
8589
8600
  model,
8590
8601
  messages: [{ role: 'user', content: 'ping' }],
@@ -8623,7 +8634,11 @@ async function testZenReasoning(key, model) {
8623
8634
  try {
8624
8635
  const res = await fetch(ZEN_BASE + '/chat/completions', {
8625
8636
  method: 'POST',
8626
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + key },
8637
+ headers: {
8638
+ 'Content-Type': 'application/json',
8639
+ Authorization: 'Bearer ' + key,
8640
+ 'x-opencode-session': OPENCODE_SESSION,
8641
+ },
8627
8642
  body: JSON.stringify({
8628
8643
  model,
8629
8644
  messages: [{ role: 'user', content: 'ping' }],
@@ -8731,7 +8746,7 @@ ipcMain.handle('custom:fetchModels', async (_e, { baseUrl, key }) => {
8731
8746
  let b = String(baseUrl || '').trim().replace(/\/+$/, '');
8732
8747
  if (!/^https?:\/\//i.test(b)) return { ok: false, error: 'URL http(s) ile başlamalı' };
8733
8748
  const url = /\/v\d+$/.test(b) ? b + '/models' : b + '/v1/models';
8734
- const headers = key ? { Authorization: 'Bearer ' + key } : {};
8749
+ const headers = { 'x-opencode-session': OPENCODE_SESSION, ...(key ? { Authorization: 'Bearer ' + key } : {}) };
8735
8750
  /* Anthropic: /v1/models yerel API — x-api-key + versiyon başlığı ister */
8736
8751
  try {
8737
8752
  if (new URL(url).hostname === 'api.anthropic.com') {