@appaflytech/zeyrek-ai-mcp 0.0.22 → 0.0.24

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 (50) hide show
  1. package/.env.example +15 -0
  2. package/dist/config.js +9 -0
  3. package/dist/index.js +2 -0
  4. package/dist/knowledge/componentCatalog.js +4 -2
  5. package/dist/knowledge/componentEvents.js +3 -3
  6. package/dist/knowledge/mobileComponentCatalog.js +308 -50
  7. package/dist/knowledge/mobileComponentEvents.js +130 -3
  8. package/dist/knowledge/normalize.js +16 -6
  9. package/dist/knowledge/platform/lint.js +110 -3
  10. package/dist/knowledge/platform/mobile.js +1 -1
  11. package/dist/knowledge/platform/mobileRules.js +70 -11
  12. package/dist/knowledge/platform/web.js +1 -1
  13. package/dist/knowledge/rules.js +106 -11
  14. package/dist/knowledge/styleResolve.js +49 -3
  15. package/dist/knowledge/styleResolveNative.js +104 -4
  16. package/dist/observability/trace.js +1 -1
  17. package/dist/platform/argumentAliases.js +128 -0
  18. package/dist/platform/bodyFile.js +103 -0
  19. package/dist/platform/capture/capture.js +29 -5
  20. package/dist/platform/capture/console-log.js +80 -0
  21. package/dist/platform/capture/index.js +1 -0
  22. package/dist/platform/diagnostics.js +10 -4
  23. package/dist/platform/http.js +23 -7
  24. package/dist/platform/responses.js +6 -2
  25. package/dist/platform/schemaReadMemory.js +60 -0
  26. package/dist/platform/secrets.js +9 -0
  27. package/dist/platform/strictArguments.js +68 -12
  28. package/dist/platform/tokenVault.js +187 -4
  29. package/dist/resources/guides/expressions.md +1 -1
  30. package/dist/resources/guides/microservice.md +9 -2
  31. package/dist/resources/guides/middleware.md +5 -1
  32. package/dist/resources/guides/mobile.md +165 -10
  33. package/dist/resources/guides/page.md +161 -41
  34. package/dist/resources/guides/theme.md +4 -1
  35. package/dist/resources/guides/workflow.md +7 -1
  36. package/dist/resources/schema-fingerprint.json +2 -2
  37. package/dist/server.js +1 -1
  38. package/dist/tools/datasources.js +1 -1
  39. package/dist/tools/designSchemaPatch.js +4 -3
  40. package/dist/tools/externalComponents.js +1 -1
  41. package/dist/tools/flows.js +70 -4
  42. package/dist/tools/layouts.js +5 -5
  43. package/dist/tools/microservices.js +26 -10
  44. package/dist/tools/pages.js +192 -36
  45. package/dist/tools/platformInfo.js +4 -1
  46. package/dist/tools/rows.js +7 -1
  47. package/dist/tools/schemaOps.js +1 -1
  48. package/dist/tools/screenWrite.js +4 -3
  49. package/dist/tools/screens.js +81 -0
  50. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ import { open, realpath } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { extname, resolve, sep } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { PlatformApiError } from './errors.js';
6
+ /**
7
+ * g-08 (`plans/mobil-plantcare-acigi`): tanım yazan araçlar gövdeyi dosyadan alabilir. Koşuda en
8
+ * büyük bağlam tasarrufu, ajanın kendi yazdığı "dosyadan MCP'ye" betiğinden geldi; o iş buraya
9
+ * taşındı. Her aracın TEK gövde argümanı vardır ve dosyanın JSON'u o argümanın değeri olur. Dosya
10
+ * SDK doğrulamasından ÖNCE okunur (`strictArguments.ts`), yani satır içi gövdeyle aynı şema ve aynı
11
+ * yol — dosyadan gelen gövde hiçbir kontrolü atlamaz.
12
+ */
13
+ export const BODY_FILE_ARGUMENTS = {
14
+ page_create: 'schema',
15
+ page_update: 'schema',
16
+ page_schema_patch: 'ops',
17
+ flow_create: 'steps',
18
+ flow_update: 'steps',
19
+ flow_patch: 'operations',
20
+ microservice_create: 'definition',
21
+ microservice_update: 'definition',
22
+ microservice_patch: 'operations',
23
+ workflow_create: 'definition',
24
+ workflow_update: 'definition',
25
+ workflow_patch: 'operations'
26
+ };
27
+ /** `ops`/`operations` eşanlamlıdır (`microOps`); ikisinden biri satır içinde varsa gövde verilmiştir. */
28
+ const synonyms = { ops: ['ops', 'operations'], operations: ['ops', 'operations'] };
29
+ const maxBodyFileBytes = 5 * 1024 * 1024;
30
+ export function bodyFileParam(argument) {
31
+ return z.string().min(1).optional().describe(`'${argument}' yerine okunacak yerel .json dosyası (en çok 5 MB): gövde ajan bağlamından geçmez, gateway dosyayı okuyup '${argument}' olarak aynı doğrulamadan geçirir. '${argument}' ile birlikte verilemez. Yol gateway sürecinin çalışma dizininin ya da MCP_INPUT_DIR'in altında olmalı (sembolik bağ çözülür). Yanıtın bodyFile alanı hangi dosyanın okunduğunu söyler.`);
32
+ }
33
+ export async function loadBodyFile(tool, args, config) {
34
+ const argument = BODY_FILE_ARGUMENTS[tool];
35
+ if (!argument || !args || typeof args !== 'object' || Array.isArray(args))
36
+ return { args };
37
+ const { bodyFile, ...rest } = args;
38
+ if (bodyFile === undefined)
39
+ return { args };
40
+ if (typeof bodyFile !== 'string' || bodyFile.length === 0)
41
+ return { args };
42
+ const inline = (synonyms[argument] ?? [argument]).filter(name => rest[name] !== undefined);
43
+ if (inline.length > 0) {
44
+ throw new PlatformApiError(`${tool}: bodyFile ile '${inline.join("', '")}' birlikte verilemez — gövde ya dosyadan ya satır içinden gelir. Birini kaldırın. İstek gönderilmedi.`, 400);
45
+ }
46
+ const absolute = resolve(bodyFile);
47
+ if (extname(absolute).toLowerCase() !== '.json') {
48
+ throw new PlatformApiError(`${tool}: bodyFile yalnız .json okur — verilen: ${absolute}. İstek gönderilmedi.`, 400);
49
+ }
50
+ let real;
51
+ try {
52
+ real = await realpath(absolute);
53
+ }
54
+ catch (error) {
55
+ throw new PlatformApiError(`${tool}: bodyFile okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`, 400);
56
+ }
57
+ if (extname(real).toLowerCase() !== '.json') {
58
+ throw new PlatformApiError(`${tool}: bodyFile'ın hedefi .json değil — ${real} (${absolute} bağının hedefi). İstek gönderilmedi.`, 400);
59
+ }
60
+ const roots = await Promise.all([process.cwd(), config.inputDirectory]
61
+ .filter((root) => typeof root === 'string' && root.length > 0)
62
+ .map(root => realpath(resolve(root)).catch(() => undefined)));
63
+ const allowed = roots.some(root => root !== undefined && (real === root || real.startsWith(`${root}${sep}`)));
64
+ if (!allowed) {
65
+ throw new PlatformApiError(`${tool}: bodyFile izinli dizinlerin dışında — ${real}${real !== absolute ? ` (${absolute} bağının hedefi)` : ''}. Gateway yalnız çalışma dizininin (${process.cwd()}) ve .env'deki MCP_INPUT_DIR'in (${config.inputDirectory ?? 'tanımsız'}) altındaki dosyaları okur. İstek gönderilmedi.`, 400);
66
+ }
67
+ // Denetim ve okuma AYNI tutamaçtan: yol denetimden sonra değişse de okunan, denetlenen dosyadır.
68
+ // O_NOFOLLOW son bileşenin sonradan bağa çevrilmesini, O_NONBLOCK `.json` adlı FIFO'nun çağrıyı
69
+ // bekletmesini önler; boyut dosyanın büyüklüğüne değil okunan bayta bakar.
70
+ let handle;
71
+ try {
72
+ handle = await open(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
73
+ }
74
+ catch (error) {
75
+ throw new PlatformApiError(`${tool}: bodyFile açılamadı — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
76
+ }
77
+ let bytes;
78
+ let text;
79
+ try {
80
+ const info = await handle.stat();
81
+ if (!info.isFile()) {
82
+ throw new PlatformApiError(`${tool}: bodyFile düzenli dosya değil — ${real}. İstek gönderilmedi.`, 400);
83
+ }
84
+ const buffer = Buffer.alloc(maxBodyFileBytes + 1);
85
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
86
+ bytes = bytesRead;
87
+ if (bytes > maxBodyFileBytes) {
88
+ throw new PlatformApiError(`${tool}: bodyFile 5 MB sınırını aşıyor. Tanımı parçalara bölün — sayfa için page_schema_patch'e birden çok batch, akış için flow_patch. İstek gönderilmedi.`, 400);
89
+ }
90
+ text = buffer.subarray(0, bytes).toString('utf8');
91
+ }
92
+ finally {
93
+ await handle.close();
94
+ }
95
+ let value;
96
+ try {
97
+ value = JSON.parse(text);
98
+ }
99
+ catch (error) {
100
+ throw new PlatformApiError(`${tool}: bodyFile JSON değil — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
101
+ }
102
+ return { args: { ...rest, [argument]: value }, note: `${bodyFile} → ${argument} (${bytes} B)` };
103
+ }
@@ -4,6 +4,7 @@ import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../../knowledge/com
4
4
  import { logStep } from '../../observability/trace.js';
5
5
  import { actionScript, activeTabScript, rootElementId, runtimePropsScript } from './actions.js';
6
6
  import { domSummaryScript } from './summary.js';
7
+ import { createPageConsoleLog, describePageFailure, parseRenderErrors, renderErrorsScript } from './console-log.js';
7
8
  import { decideReadiness, describeUnready, parseReadiness, readinessScript, runtimePath } from './readiness.js';
8
9
  /**
9
10
  * Ziyaretçinin tema tercihinin yaşadığı localStorage anahtarı — `theme-toggle` bunu yazar,
@@ -17,6 +18,12 @@ const VIEWER_MODE_STORAGE_KEY = 'zeyrek-viewer-theme-mode';
17
18
  * büyüterek alınır: Studio runtime'ı kendi kaydırma kabını `100vh` üzerine kuruyor ve viewport
18
19
  * ötesi yakalama o kabı bir kez daha çiziyordu — aynı sayfa karede iki kez görünüyordu (09-04
19
20
  * Y-23). Tek karede çekilen görüntüde o birleştirme adımı hiç yok.
21
+ *
22
+ * İç kaydırıcı için gereken viewport `innerHeight + (scrollHeight − clientHeight)`'tir, kabın
23
+ * `scrollHeight`'i DEĞİL: kap viewport'la birlikte büyür ama üstündeki/altındaki sabit parçalar
24
+ * (mobil host'ta app bar + tab bar, ZM-23 f-06) büyümez. Yalnız `scrollHeight` itilince viewport
25
+ * içerik boyuna çıkıyor, kap hâlâ o parçalar kadar kısa kalıyor ve uzun mobil sayfanın altı
26
+ * karenin dışında kalıyordu. Kap viewport kadar yüksekse iki formül aynı sonucu verir.
20
27
  */
21
28
  export const contentHeightScript = [
22
29
  '(function () {',
@@ -27,7 +34,7 @@ export const contentHeightScript = [
27
34
  ' var nodes = document.querySelectorAll("*");',
28
35
  ' for (var index = 0; index < nodes.length && index < 4000; index += 1) {',
29
36
  ' var node = nodes[index];',
30
- ' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(node.scrollHeight);',
37
+ ' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(viewportHeight + node.scrollHeight - node.clientHeight);',
31
38
  ' }',
32
39
  ' return { contentHeight: Math.max.apply(null, heights), viewportHeight: viewportHeight };',
33
40
  '})()'
@@ -91,6 +98,10 @@ export const layoutReportScript = [
91
98
  ' };',
92
99
  '})'
93
100
  ].join('\n');
101
+ /** Mobil çizicinin hata sınırı işaretleri; betik düşerse boş (çekimi düşürmez). */
102
+ async function readRenderErrors(page) {
103
+ return page.evaluate(renderErrorsScript).then(parseRenderErrors).catch(() => []);
104
+ }
94
105
  /**
95
106
  * F-09 — sayfa akışı yalnız tarayıcıda koşar, bu yüzden bir setter iteratee'sinin
96
107
  * patlamasının gateway'e ulaşan HİÇBİR yolu yoktu: akış 200 dönüyor, hedef prop
@@ -264,7 +275,7 @@ export class StudioScreenshotter {
264
275
  * işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
265
276
  * yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
266
277
  */
267
- async runActions(page, request) {
278
+ async runActions(page, request, consoleLog) {
268
279
  if (!request.actions || request.actions.length === 0)
269
280
  return undefined;
270
281
  const outcomes = [];
@@ -306,7 +317,8 @@ export class StudioScreenshotter {
306
317
  completedBefore: activityBefore?.completed ?? Number.POSITIVE_INFINITY
307
318
  });
308
319
  if (readiness.state === 'not-ready' && request.captureUnready !== true) {
309
- throw new PlatformApiError(`Etkileşim adımı ${index} (${action.action}${action.target === undefined ? '' : ` '${action.target}'`}) sonrasında sayfa ${readiness.waitedMs} ms içinde durulmadı, kare ALINMADI — ${describeUnready(readiness)}. Bekleme bütçesi ${apiResponseTimeoutMs + request.delayMs} ms; akış gerçekten uzun sürüyorsa delayMs ile büyütün. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
320
+ const failure = consoleLog ? describePageFailure(consoleLog, await readRenderErrors(page)) : '';
321
+ throw new PlatformApiError(`Etkileşim adımı ${index} (${action.action}${action.target === undefined ? '' : ` '${action.target}'`}) sonrasında sayfa ${readiness.waitedMs} ms içinde durulmadı, kare ALINMADI — ${describeUnready(readiness)}.${failure} Bekleme bütçesi ${apiResponseTimeoutMs + request.delayMs} ms; akış gerçekten uzun sürüyorsa delayMs ile büyütün. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
310
322
  }
311
323
  const after = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => undefined);
312
324
  const requests = typeof after === 'number' && typeof before.requests === 'number' ? Math.max(after - before.requests, 0) : 0;
@@ -459,6 +471,11 @@ export class StudioScreenshotter {
459
471
  await context.addInitScript(`try { localStorage.setItem(${JSON.stringify(VIEWER_MODE_STORAGE_KEY)}, ${JSON.stringify(request.colorScheme)}) } catch (error) { /* localStorage kapalıysa proje varsayılanı çizilir */ }`);
460
472
  }
461
473
  const page = await context.newPage();
474
+ // Konsol ve istisna dinleyicileri navigasyondan ÖNCE takılır: modül yüklenirken patlayan
475
+ // bileşenin hatası ilk karede düşer ve sonradan takılan dinleyici onu kaçırırdı (g-06).
476
+ const consoleLog = createPageConsoleLog();
477
+ page.on?.('console', message => consoleLog.onConsole(message));
478
+ page.on?.('pageerror', error => consoleLog.onPageError(error));
462
479
  // Soğuk Vite derlemesinde sayfanın kendi veri isteği 15 sn'yi aşabiliyor; delayMs bu
463
480
  // bütçeyi de uzatır (09-02 E-2). Yanıt hazırlık döngüsünü BLOKLAMAZ: runtime dışına
464
481
  // yönlendirilen sayfa bu isteği hiç atmaz ve eskiden 15 sn boşa bekleniyordu.
@@ -499,7 +516,8 @@ export class StudioScreenshotter {
499
516
  const apiStatus = await Promise.race([observedApiStatus, Promise.resolve(undefined)]);
500
517
  const apiNote = apiStatus === undefined ? 'sayfanın veri isteği gözlenmedi' : `veri isteği HTTP ${apiStatus}`;
501
518
  if (readiness.state === 'not-ready' && request.captureUnready !== true) {
502
- throw new PlatformApiError(`Sayfa ${readiness.waitedMs} ms içinde yüklenmedi, kare ALINMADI — ${describeUnready(readiness, apiNote)}. Bekleme bütçesi ${readyBudgetMs} ms; sayfa yalnız yavaşsa delayMs ile büyütün. Sayfa gerçekten bozuksa page_runtime_get ile şemayı, STUDIO_BASE_URL ve slug'ları doğrulayın. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
519
+ const failure = describePageFailure(consoleLog, await readRenderErrors(page));
520
+ throw new PlatformApiError(`Sayfa ${readiness.waitedMs} ms içinde yüklenmedi, kare ALINMADI — ${describeUnready(readiness, apiNote)}.${failure} Bekleme bütçesi ${readyBudgetMs} ms; sayfa yalnız yavaşsa delayMs ile büyütün. Sayfa gerçekten bozuksa page_runtime_get ile şemayı, STUDIO_BASE_URL ve slug'ları doğrulayın. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
503
521
  }
504
522
  await page.evaluate('document.fonts.ready.then(() => true)').catch(() => undefined);
505
523
  const flowsSettled = readiness.state === 'ready' || readiness.state === 'ready-no-flows'
@@ -507,7 +525,7 @@ export class StudioScreenshotter {
507
525
  : readiness.state === 'not-ready' ? false : undefined;
508
526
  const finalHref = readiness.snapshot?.href;
509
527
  const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
510
- const actions = await this.runActions(page, request);
528
+ const actions = await this.runActions(page, request, consoleLog);
511
529
  // Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
512
530
  // boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
513
531
  // tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
@@ -548,6 +566,9 @@ export class StudioScreenshotter {
548
566
  const runtimeErrors = await page.evaluate(flowRuntimeErrorsScript)
549
567
  .then(value => Array.isArray(value) ? value : [])
550
568
  .catch(() => []);
569
+ const renderErrors = await readRenderErrors(page);
570
+ const consoleErrors = consoleLog.consoleErrors();
571
+ const pageErrors = consoleLog.pageErrors();
551
572
  // Özet kareden ÖNCE ve büyütmeden SONRA: ikisi de aynı yerleşmiş sayfayı anlatsın.
552
573
  const summaryRaw = request.summary === false
553
574
  ? undefined
@@ -590,6 +611,9 @@ export class StudioScreenshotter {
590
611
  ...(layout === undefined ? {} : { layout }),
591
612
  ...(actions === undefined ? {} : { actions }),
592
613
  ...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
614
+ ...(consoleErrors.length > 0 ? { consoleErrors } : {}),
615
+ ...(pageErrors.length > 0 ? { pageErrors } : {}),
616
+ ...(renderErrors.length > 0 ? { renderErrors } : {}),
593
617
  ...(flowsSettled === undefined ? {} : { flowsSettled }),
594
618
  ...(navigationRetried ? { navigationRetried: true } : {})
595
619
  };
@@ -0,0 +1,80 @@
1
+ import { maskSecretText } from '../secrets.js';
2
+ const bufferLimit = 20;
3
+ const reportLimit = 5;
4
+ const textLimit = 300;
5
+ const stackLines = 5;
6
+ const clip = (text) => (text.length > textLimit ? `${text.slice(0, textLimit - 1)}…` : text);
7
+ /**
8
+ * Çekim boyunca tarayıcı konsolunun hata/uyarı satırları ve yakalanmamış istisnalar. Koşuda sayfa
9
+ * çizilmediğinde `page_screenshot` yalnız "runtime hiç render olmadı" diyordu; nedeni bulmak için
10
+ * ajan kendi Playwright betiğini yazıp Studio paketinin sıkıştırılmış kaynağını okudu
11
+ * (plans/mobil-plantcare-acigi g-06). Aynı metin tekrarlanırsa sayaç artar, satır eklenmez; metin
12
+ * kırpılır ve kimlik parçaları maskelenir.
13
+ */
14
+ export function createPageConsoleLog() {
15
+ const consoleRecords = [];
16
+ const pageErrorRecords = [];
17
+ return {
18
+ onConsole(message) {
19
+ const type = message.type();
20
+ const level = type === 'error' ? 'error' : type === 'warning' ? 'warning' : undefined;
21
+ if (!level)
22
+ return;
23
+ const text = clip(maskSecretText(message.text()));
24
+ const existing = consoleRecords.find(record => record.level === level && record.text === text);
25
+ if (existing)
26
+ existing.count += 1;
27
+ else if (consoleRecords.length < bufferLimit)
28
+ consoleRecords.push({ level, text, count: 1 });
29
+ },
30
+ onPageError(error) {
31
+ if (pageErrorRecords.length >= bufferLimit)
32
+ return;
33
+ const message = clip(maskSecretText(error instanceof Error ? `${error.name}: ${error.message}` : String(error)));
34
+ const stack = error instanceof Error && error.stack
35
+ ? maskSecretText(error.stack.split('\n').slice(1, stackLines + 1).map(line => line.trim()).join('\n'))
36
+ : undefined;
37
+ if (!pageErrorRecords.some(record => record.message === message))
38
+ pageErrorRecords.push({ message, ...(stack ? { stack } : {}) });
39
+ },
40
+ /** Önce hatalar, sonra uyarılar; en çok beş kayıt. */
41
+ consoleErrors() {
42
+ return [...consoleRecords.filter(record => record.level === 'error'), ...consoleRecords.filter(record => record.level === 'warning')].slice(0, reportLimit);
43
+ },
44
+ pageErrors() {
45
+ return pageErrorRecords.slice(0, reportLimit);
46
+ }
47
+ };
48
+ }
49
+ const failureLimit = 3;
50
+ /**
51
+ * Çizilemeyen sayfanın hata metnine eklenen nedenler: çöken düğümler, yakalanmamış istisnalar, konsol
52
+ * hataları — bu sırayla, her birinden en çok üç; uyarılar eklenmez. Hiçbiri yoksa boş. Yalnız ilk neden
53
+ * yazılırken sayfa istisnası React'in bileşeni adlandıran konsol satırını gizliyordu.
54
+ */
55
+ export function describePageFailure(log, renderErrors) {
56
+ const crashed = renderErrors.slice(0, failureLimit).map(record => `${record.elementId ?? '?'} — ${record.message}`);
57
+ const thrown = log.pageErrors().slice(0, failureLimit).map(record => `${record.message}${record.stack ? ` (${record.stack.split('\n')[0]})` : ''}`);
58
+ const logged = log.consoleErrors().filter(record => record.level === 'error').slice(0, failureLimit).map(record => record.text);
59
+ return [
60
+ crashed.length ? ` Çöken düğüm: ${crashed.join('; ')}.` : '',
61
+ thrown.length ? ` Sayfa istisnası: ${thrown.join('; ')}.` : '',
62
+ logged.length ? ` Konsol hatası: ${logged.join('; ')}.` : ''
63
+ ].join('');
64
+ }
65
+ /**
66
+ * Mobil çizicinin hata sınırlarının bıraktığı işaretler (`data-ng-render-error`). Düğümün dış sınırı
67
+ * (kancaları çöken düğüm) yer tutucuyu kendi kabuğunun dışında çizer ve kimliği işaretin üstünde taşır
68
+ * (`data-ng-render-error-id`); taşımıyorsa en yakın kabuk düğümdür.
69
+ */
70
+ export const renderErrorsScript = "Array.from(document.querySelectorAll('[data-ng-render-error]')).slice(0, 20).map(function (el) { var own = el.getAttribute('data-ng-render-error-id'); var owner = own ? null : el.closest('[data-element-id]'); return { elementId: own || (owner ? owner.getAttribute('data-element-id') : undefined), message: el.getAttribute('data-ng-render-error') || '' }; })";
71
+ export function parseRenderErrors(value) {
72
+ if (!Array.isArray(value))
73
+ return [];
74
+ return value
75
+ .filter((entry) => entry !== null && typeof entry === 'object')
76
+ .map(entry => ({
77
+ ...(typeof entry.elementId === 'string' ? { elementId: entry.elementId } : {}),
78
+ message: clip(maskSecretText(typeof entry.message === 'string' ? entry.message : ''))
79
+ }));
80
+ }
@@ -8,3 +8,4 @@ export * from './actions.js';
8
8
  export * from './summary.js';
9
9
  export * from './readiness.js';
10
10
  export * from './capture.js';
11
+ export * from './console-log.js';
@@ -5,8 +5,9 @@ const MAX_NOTES = 8;
5
5
  const MAX_NOTE_CHARS = 240;
6
6
  const MAX_DROPS = 5;
7
7
  export const diagnosticsParam = z.enum(['compact', 'full']).optional().describe("Yanıttaki gatewayNormalized / silentlyDropped listelerinin boyu. compact (varsayılan): aynı not tekilleşir, uzun not kısalır (en fazla 8); aynı yolun düşmeleri tek satırda sayıyla toplanır (en fazla 5 grup); droppedCount her zaman tam sayıdır. full: listelerin tamamı.");
8
- export function runWithDiagnostics(mode, run) {
9
- return activeMode.run(mode, run);
8
+ /** `adjustable`: tool `diagnostics` parametresini alıyor mu — kısaltma notu yalnız alan tool'a onu önerir (g-08). */
9
+ export function runWithDiagnostics(mode, run, adjustable = true) {
10
+ return activeMode.run({ mode, adjustable }, run);
10
11
  }
11
12
  /**
12
13
  * Tanılama listeleri her yazma çağrısında yanıta girer ve aynı gerekçe cümlesi çağrı başına tekrar
@@ -18,7 +19,7 @@ export function runWithDiagnostics(mode, run) {
18
19
  * listeyi görür; kısaltma yalnız `diagnostics` parametresini taşıyan yazma tool'larında devreye girer.
19
20
  */
20
21
  export function compactDiagnostics(payload) {
21
- if (activeMode.getStore() !== 'compact')
22
+ if (activeMode.getStore()?.mode !== 'compact')
22
23
  return payload;
23
24
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
24
25
  return payload;
@@ -58,7 +59,12 @@ export function compactDiagnostics(payload) {
58
59
  }
59
60
  if (hidden.length === 0)
60
61
  return result;
61
- return { ...result, diagnosticsNote: "Kısaltıldı; yazmayı tekrarlamayın. Tam liste: sonraki çağrıda diagnostics:'full'." };
62
+ return {
63
+ ...result,
64
+ diagnosticsNote: activeMode.getStore()?.adjustable === false
65
+ ? 'Kısaltıldı; tam liste gateway günlüğünde (bu tool diagnostics parametresi almaz).'
66
+ : "Kısaltıldı; yazmayı tekrarlamayın. Tam liste: sonraki çağrıda diagnostics:'full'."
67
+ };
62
68
  }
63
69
  /**
64
70
  * Aynı yolun farklı öğelerindeki düşmeler (`selectFields[fieldId=a].path`, `selectFields[fieldId=b].path`, …)
@@ -6,6 +6,20 @@ import { randomUUID } from 'node:crypto';
6
6
  import { request } from 'undici';
7
7
  export const requestAuthStorage = new AsyncLocalStorage();
8
8
  const hiddenResponseHeaders = new Set(['set-cookie', 'authorization', 'proxy-authorization']);
9
+ /**
10
+ * g-08: yürütme yanıtının varsayılan başlıkları — işin okuduğu olanlar. `total-count` sayfalı tablo
11
+ * akışlarının sözleşmesidir (İK koşusu M-8), `location` yönlendirmenin, `x-request-id`/`traceparent`
12
+ * platform günlüğünde aynı isteği bulmanın anahtarıdır. Cloudflare ve sunucu başlıkları (`cf-ray`,
13
+ * `server`, `alt-svc`, `date` …) ajan bağlamında yer kaplıyordu ve hiçbir karar onlara bakmıyordu.
14
+ */
15
+ const essentialResponseHeaders = new Set(['content-type', 'content-length', 'content-disposition', 'location', 'total-count', 'x-request-id', 'traceparent']);
16
+ export function headersForMode(headers, mode) {
17
+ if (mode === 'none')
18
+ return undefined;
19
+ if (mode === 'all')
20
+ return headers;
21
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => essentialResponseHeaders.has(name)));
22
+ }
9
23
  /**
10
24
  * Sayfalı tablo akışları `Total-Count` header'ına bağlı ama execute ailesi yalnız gövdeyi
11
25
  * döndürüyordu; sözleşme hiçbir tool'dan doğrulanamıyordu (İK koşusu M-8). Gizli header'lar
@@ -153,13 +167,15 @@ export class Platform {
153
167
  body = JSON.stringify(options.body);
154
168
  }
155
169
  const startedAt = Date.now();
156
- logStep('istek →', `${method} ${url.href} · ${api} api${options.environment ? ` · env=${options.environment}` : ''}${options.anonymous ? ' · ANONİM (Authorization header YOK)' : ''}${isRetry ? ' · 401 sonrası tekrar' : ''}`, logsDetail() && (options.multipart || options.form || options.body !== undefined)
157
- ? [`gövde: ${renderJson(options.multipart
158
- ? describeMultipart(options.multipart)
159
- : options.form
160
- ? Object.fromEntries(options.form)
161
- : options.body)}`]
162
- : []);
170
+ logStep('istek →', `${method} ${url.href} · ${api} api${options.environment ? ` · env=${options.environment}` : ''}${options.anonymous ? ' · ANONİM (Authorization header YOK)' : ''}${isRetry ? ' · 401 sonrası tekrar' : ''}`, options.hideRequestBody === true
171
+ ? ['gövde: (gizli — kimlik bilgisi taşıyor, günlüğe yazılmaz)']
172
+ : logsDetail() && (options.multipart || options.form || options.body !== undefined)
173
+ ? [`gövde: ${renderJson(options.multipart
174
+ ? describeMultipart(options.multipart)
175
+ : options.form
176
+ ? Object.fromEntries(options.form)
177
+ : options.body)}`]
178
+ : []);
163
179
  let statusCode;
164
180
  let payload;
165
181
  let responseBytes = 0;
@@ -1,4 +1,6 @@
1
1
  import { compactDiagnostics } from './diagnostics.js';
2
+ import { argumentNotes } from './argumentAliases.js';
3
+ import { tokenTtlMinutes } from './tokenVault.js';
2
4
  import { logError, logStep } from '../observability/trace.js';
3
5
  import { PlatformApiError, describeErrorBody } from './errors.js';
4
6
  import { PolicyBlockError } from '../knowledge/policy.js';
@@ -162,8 +164,10 @@ export function toolResult(payload, config, expectFailure) {
162
164
  ...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
163
165
  ...(runtimeErrors.length > 0 && expectFailure === true ? { expectedFailure: true } : {}),
164
166
  ...(masked.length > 0 ? { gatewayMasked: masked } : {}),
165
- ...(Object.keys(tokenRefs).length > 0 ? { tokenRefs, tokenRefNote: "Maskelenen JWT'ler gateway içinde tutuldu; bu referansları page_screenshot / page_runtime_get 'asUser' alanına verebilirsiniz (15 dk geçerli). Jetonun kendisi yanıta ve günlüğe hiç girmez." } : {}),
166
- ...(truncated ? { truncated: true, truncationNote: 'Liste, yanıt boyutu sınırı nedeniyle kırpıldı; sayfalama ya da daha dar filtre kullanın.' } : {})
167
+ ...(Object.keys(tokenRefs).length > 0 ? { tokenRefs, tokenRefNote: `Maskelenen JWT'ler gateway içinde tutuldu; bu referansları page_screenshot / page_runtime_get 'asUser' alanına verebilirsiniz (${tokenTtlMinutes()} dk geçerli). Jetonun kendisi yanıta ve günlüğe hiç girmez.` } : {}),
168
+ ...(truncated ? { truncated: true, truncationNote: 'Liste, yanıt boyutu sınırı nedeniyle kırpıldı; sayfalama ya da daha dar filtre kullanın.' } : {}),
169
+ ...(argumentNotes().aliasesApplied.length > 0 ? { aliasesApplied: argumentNotes().aliasesApplied } : {}),
170
+ ...(argumentNotes().bodyFile ? { bodyFile: argumentNotes().bodyFile } : {})
167
171
  };
168
172
  return {
169
173
  content: [{ type: 'text', text: JSON.stringify(enriched) }],
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Sayfa okumalarının belleği. İki soru cevaplar:
3
+ *
4
+ * - "Bu sayfa sürümü kırpık okundu mu" (ZM-28, PAGE-038): `page_schema_get(mode:'full',
5
+ * allowTruncated:true)` sığmayan şemayı kırparak döndüğünde anahtar işaretlenir; aynı sürümü
6
+ * `baseVersion` olarak veren `page_schema_patch` uyarı taşır — plan, düşen düğümleri görmeden
7
+ * yapılmış olabilir.
8
+ * - "Bu gateway sayfanın hangi sürümünü en son gördü" (PAGE-039, `plans/mobil-plantcare-acigi`
9
+ * g-05): `page_get`, `page_schema_get`, `page_schema_patch`'in `newVersion`'ı ve `page_update`'in
10
+ * kendi yazdığı sürüm hatırlanır; `page_update` `baseVersion` verilmediğinde onu gönderir. Hiç
11
+ * okunmamış sayfaya sürümsüz tam gövde yazılmaz.
12
+ *
13
+ * SÜREÇ İÇİNDEDİR ve yalnız anahtar/sürüm tutar (gövde yok); `CaptureMemory` ile aynı halka:
14
+ * yeniden yazılan anahtar sona gider, en eskisi düşer.
15
+ */
16
+ export function schemaReadKey(project, environment, pageId, version) {
17
+ return `${project}/${environment}/${pageId}@${version}`;
18
+ }
19
+ export function pageReadKey(project, environment, pageId) {
20
+ return `${project}/${environment}/${pageId}`;
21
+ }
22
+ export class SchemaReadMemory {
23
+ limit;
24
+ truncated = new Set();
25
+ versions = new Map();
26
+ constructor(limit = 200) {
27
+ this.limit = limit;
28
+ }
29
+ rememberVersion(key, version) {
30
+ this.versions.delete(key);
31
+ this.versions.set(key, version);
32
+ while (this.versions.size > this.limit) {
33
+ const oldest = this.versions.keys().next().value;
34
+ if (oldest === undefined)
35
+ break;
36
+ this.versions.delete(oldest);
37
+ }
38
+ }
39
+ lastVersion(key) {
40
+ return this.versions.get(key);
41
+ }
42
+ markTruncated(key) {
43
+ this.truncated.delete(key);
44
+ this.truncated.add(key);
45
+ while (this.truncated.size > this.limit) {
46
+ const oldest = this.truncated.values().next().value;
47
+ if (oldest === undefined)
48
+ break;
49
+ this.truncated.delete(oldest);
50
+ }
51
+ }
52
+ wasTruncated(key) {
53
+ return this.truncated.has(key);
54
+ }
55
+ }
56
+ /**
57
+ * SÜREÇ düzeyinde tek bellek: HTTP taşıması her POST için yeni bir gateway sunucusu kurduğundan
58
+ * (`sharedCaptureMemory` notu) okuma ile yazma ayrı `registerPageTools` çağrılarında koşar.
59
+ */
60
+ export const sharedSchemaReadMemory = new SchemaReadMemory();
@@ -38,3 +38,12 @@ export function maskSecrets(payload) {
38
38
  const tokenRefs = {};
39
39
  return { payload: walk(payload, '', masked, tokenRefs, 0), masked, tokenRefs };
40
40
  }
41
+ /**
42
+ * Serbest METİNDEKİ kimlik parçaları (konsol satırı, hata yığını): JWT biçimli dize ve `Bearer …`.
43
+ * `maskSecrets` anahtar adına bakar; tarayıcı konsolu anahtarsız düz metin taşır (g-06).
44
+ */
45
+ export function maskSecretText(text) {
46
+ return text
47
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${maskedValue}`)
48
+ .replace(/\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, maskedValue);
49
+ }
@@ -1,5 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import { diagnosticsParam, runWithDiagnostics } from './diagnostics.js';
3
+ import { applyArgumentAliases, closestArgument, runWithArgumentNotes } from './argumentAliases.js';
4
+ import { BODY_FILE_ARGUMENTS, bodyFileParam, loadBodyFile } from './bodyFile.js';
5
+ import { PlatformApiError } from './errors.js';
6
+ import { logError } from '../observability/trace.js';
7
+ /** Doğrulanmış argümanlara takılan çeviri notları (`ArgumentNotes`); JSON'a ve platform isteğine girmez. */
8
+ const ARGUMENT_NOTES = Symbol('argumentNotes');
3
9
  /**
4
10
  * `writes`/`updates` açıklamalı ama KAYIT yazmayan tool'lar: çalıştırma, önizleme, dışa aktarma, indirme,
5
11
  * bağlantı testi. Yanıtlarında yazma tanılaması (silentlyDropped) yoktur; parametre yalnız tool listesini
@@ -25,25 +31,75 @@ function isRawShape(value) {
25
31
  * olmayan bir sorgu kusuruna yönlendirdi. Tek tek tool'lara alan eklemek sınıfı kapatmaz; sessizlik
26
32
  * burada, SDK'ya şema verilmeden önce kaldırılır.
27
33
  */
28
- export function rejectUnknownArguments(server) {
34
+ export function rejectUnknownArguments(server, platform) {
29
35
  const registerTool = server.registerTool.bind(server);
36
+ // Zorunlu gövde argümanı `bodyFile` açıkken şemada isteğe bağlı yayınlanır (yoksa şemaya uyan istemci
37
+ // gövdeyi satır içinde yollamak zorunda kalır ve `bodyFile` "birlikte verilemez" ile reddedilirdi);
38
+ // zorunluluk dosya okunduktan sonra burada denetlenir.
39
+ const requiredBodies = new Map();
40
+ // g-07/g-08: `bodyFile` ve takma ad tablosu SDK'nın strict doğrulamasından ÖNCE koşmalı — sonra
41
+ // koşsa yanlış ad zaten reddedilmiş, dosyadan gelen gövde doğrulanmamış olur. SDK doğrulamayı
42
+ // `validateToolInput` içinde yapar; çeviri orada uygulanır ve notlar doğrulanmış argümanlara
43
+ // sembol anahtarla takılır.
44
+ if (platform) {
45
+ const validator = server;
46
+ const validate = validator.validateToolInput.bind(server);
47
+ validator.validateToolInput = async (tool, args, name) => {
48
+ try {
49
+ const loaded = await loadBodyFile(name, args, platform.config);
50
+ const requiredBody = requiredBodies.get(name);
51
+ if (requiredBody && loaded.args?.[requiredBody] === undefined) {
52
+ throw new PlatformApiError(`${name}: '${requiredBody}' zorunlu — satır içinde ya da bodyFile ile (.json dosyası) verin. İstek gönderilmedi.`, 400);
53
+ }
54
+ const { args: aliased, applied } = await applyArgumentAliases(name, loaded.args, { platform, shape: tool.inputSchema?.shape ?? {} });
55
+ const parsed = await validate(tool, aliased, name);
56
+ const notes = { aliasesApplied: applied, ...(loaded.note ? { bodyFile: loaded.note } : {}) };
57
+ return (applied.length > 0 || loaded.note) && parsed && typeof parsed === 'object' ? Object.assign(parsed, { [ARGUMENT_NOTES]: notes }) : parsed;
58
+ }
59
+ catch (error) {
60
+ // Doğrulamadan önceki çeviri (dosya, takma ad) `handle`'ın dışında koşar: hata burada günlüğe yazılır.
61
+ if (error instanceof PlatformApiError)
62
+ logError(error);
63
+ throw error;
64
+ }
65
+ };
66
+ }
30
67
  server.registerTool = ((name, config, callback) => {
31
68
  if (!isRawShape(config.inputSchema))
32
69
  return registerTool(name, config, callback);
33
- // Yazma tool'ları tanılama kipini alır; parametre handler'a ULAŞMAZ (gövdeyi `...rest` ile
34
- // platforma geçiren tool'larda sızardı), yalnız toolResult'ın okuduğu bağlama konur.
35
- const writesDiagnostics = config.annotations?.readOnlyHint === false && config.annotations.destructiveHint !== true && !NON_WRITING_TOOLS.has(name);
36
- const shape = writesDiagnostics ? { ...config.inputSchema, diagnostics: diagnosticsParam } : config.inputSchema;
37
- const run = writesDiagnostics
38
- ? ((args, extra) => {
39
- const { diagnostics, ...rest } = args ?? {};
40
- return runWithDiagnostics(diagnostics === 'full' ? 'full' : 'compact', () => callback(rest, extra));
41
- })
42
- : callback;
70
+ // Tanılama notları her tool'da compact döner (g-08: okuma ve çalıştırma yanıtında da 8 not +
71
+ // "N not daha"). `diagnostics` parametresi yalnız kayıt yazan tool'lara, microservice_execute'a ve
72
+ // *_preview'a açılır — ötekilerde tool listesini büyütmesin diye varsayılan kullanılır. Parametre
73
+ // handler'a ULAŞMAZ (gövdeyi `...rest` ile platforma geçiren tool'larda sızardı), yalnız
74
+ // toolResult'ın okuduğu bağlama konur.
75
+ const writesRecords = config.annotations?.readOnlyHint === false && config.annotations.destructiveHint !== true && !NON_WRITING_TOOLS.has(name);
76
+ const exposesDiagnostics = writesRecords || name === 'microservice_execute' || name.endsWith('_preview');
77
+ const bodyArgument = BODY_FILE_ARGUMENTS[name];
78
+ const bodySchema = bodyArgument ? config.inputSchema[bodyArgument] : undefined;
79
+ if (bodyArgument && bodySchema && !bodySchema.isOptional())
80
+ requiredBodies.set(name, bodyArgument);
81
+ const shape = {
82
+ ...config.inputSchema,
83
+ ...(bodyArgument && bodySchema ? { [bodyArgument]: bodySchema.optional(), bodyFile: bodyFileParam(bodyArgument) } : {}),
84
+ ...(exposesDiagnostics ? { diagnostics: diagnosticsParam } : {})
85
+ };
86
+ const invoke = (args, extra) => {
87
+ const { diagnostics, ...rest } = exposesDiagnostics ? args ?? {} : { ...args, diagnostics: undefined };
88
+ return runWithDiagnostics(diagnostics === 'full' ? 'full' : 'compact', () => callback(exposesDiagnostics ? rest : args, extra), exposesDiagnostics);
89
+ };
90
+ const run = (args, extra) => {
91
+ const { [ARGUMENT_NOTES]: notes, ...rest } = args ?? {};
92
+ return runWithArgumentNotes(notes ?? { aliasesApplied: [] }, () => invoke(rest, extra));
93
+ };
43
94
  const accepted = Object.keys(shape).join(', ');
95
+ const suggestions = (keys) => keys
96
+ .map(key => ({ key, near: closestArgument(key, Object.keys(shape)) }))
97
+ .filter(entry => entry.near !== undefined)
98
+ .map(entry => ` '${entry.key}' → '${entry.near}' mi demek istediniz?`)
99
+ .join('');
44
100
  const inputSchema = z.object(shape, {
45
101
  errorMap: (issue, context) => issue.code === 'unrecognized_keys'
46
- ? { message: `${name} şu üst düzey argümanları tanımıyor: ${issue.keys.map(key => `'${key}'`).join(', ')}. Tanınmayan alan sessizce yok sayılmaz — çağrı hiç gönderilmedi. Bu tool'un okuduğu alanlar: ${accepted}.` }
102
+ ? { message: `${name} şu üst düzey argümanları tanımıyor: ${issue.keys.map(key => `'${key}'`).join(', ')}. Tanınmayan alan sessizce yok sayılmaz — çağrı hiç gönderilmedi. Bu tool'un okuduğu alanlar: ${accepted}.${suggestions(issue.keys)}` }
47
103
  : { message: context.defaultError }
48
104
  }).strict();
49
105
  return registerTool(name, { ...config, inputSchema }, run);