@appaflytech/zeyrek-ai-mcp 0.0.52 → 0.0.54

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 (43) hide show
  1. package/dist/knowledge/componentCatalog.js +3 -1
  2. package/dist/knowledge/componentEvents.js +13 -1
  3. package/dist/knowledge/mobileComponentCatalog.js +4 -1
  4. package/dist/knowledge/normalize.js +4 -3
  5. package/dist/platform/argumentAliases.js +13 -4
  6. package/dist/platform/bodyFile.js +195 -38
  7. package/dist/platform/capture/actions.js +24 -5
  8. package/dist/platform/capture/capture.js +80 -11
  9. package/dist/platform/capture/persistent-browser.js +23 -1
  10. package/dist/platform/capture/readiness.js +32 -1
  11. package/dist/platform/errors.js +45 -5
  12. package/dist/platform/http.js +17 -9
  13. package/dist/platform/responses.js +7 -1
  14. package/dist/platform/secrets.js +8 -0
  15. package/dist/platform/token.js +32 -21
  16. package/dist/resources/examples/mobile-offline-list.json +1 -1
  17. package/dist/resources/examples/mobile-offline-queue.json +1 -1
  18. package/dist/resources/examples/mobile-offline-seamless.json +12 -3
  19. package/dist/resources/guides/deployment.md +9 -0
  20. package/dist/resources/guides/merge.md +5 -0
  21. package/dist/resources/guides/microservice.md +13 -1
  22. package/dist/resources/guides/mobile-publish.md +7 -4
  23. package/dist/resources/guides/mobile-steps.md +40 -3
  24. package/dist/resources/guides/mobile.md +39 -8
  25. package/dist/resources/guides/page.md +39 -8
  26. package/dist/resources/guides/translation.md +3 -1
  27. package/dist/resources/prompts/mobile-publish-prepare.md +3 -2
  28. package/dist/resources/schema-fingerprint.json +2 -2
  29. package/dist/tools/applications.js +9 -4
  30. package/dist/tools/connectData.js +13 -4
  31. package/dist/tools/deployments.js +1 -1
  32. package/dist/tools/designSchemaPatch.js +7 -2
  33. package/dist/tools/environments.js +40 -0
  34. package/dist/tools/flows.js +30 -3
  35. package/dist/tools/microservices.js +25 -7
  36. package/dist/tools/mobile.js +56 -0
  37. package/dist/tools/mobileStore.js +2 -2
  38. package/dist/tools/pages.js +69 -16
  39. package/dist/tools/platformInfo.js +1 -1
  40. package/dist/tools/screenWrite.js +107 -1
  41. package/dist/tools/variables.js +1 -1
  42. package/package.json +1 -1
  43. package/dist/resources/guides/mobile-flow.md +0 -64
@@ -502,6 +502,7 @@ export const componentKnowledge = {
502
502
  summary: 'Aranabilir çoklu seçim; value bir değer dizisidir, seçilenler etiket (chip) olarak görünür, search olayı sunucu taraflı arama kurmaya yarar.',
503
503
  behavior: [
504
504
  "Bakma penceresi (lookupMode modal | drawer) tablo kelimelerini (Uygula, Seçimi temizle, ipucu satırı, filtre çubuğu, işleç adları) tablonun satırlarından okur: compProp.datatable.<kelime> (2026-10-15, frontend ADR 0024); bu satırlar bileşenin Translations sekmesinde tablonun anahtarlarıyla listelenir ve bileşen kullanılınca tohumlanır. Düğümün texts'i önce gelir.",
505
+ "Bakma penceresinde başlık satırının ilk hücresi tümünü seç kutusudur (2026-11-02): süzgece uyan BÜTÜN kayıtları seçer, sonuç sayfaya sığmıyorsa kimlikleri bir kez okur (en çok 1000; fazlasında pencere söyler). Süzgecin dışındaki önceki seçim korunur, yazım yine Uygula iledir. Datatable çoka-çok hücresi ve çoklu filtre penceresi aynı pencereyi kullanır.",
505
506
  "value bir DİZİDİR ve üç biçimi de okunur: düz değer (['policies', 7]), şemanın biçimi ([{ value, label }]) ve eski ikizler ([{ id, text }]). Düz değer yazmak 2026-09-11'e kadar etiketi 'undefined' gösteriyordu; artık değer etiketin kendisi olur ve doğru metin options yüklenince gelir (2026-10-01'e kadar gelmiyordu: akışın yazdığı id dizisi seçenekler yüklense de çipte id olarak kalıyordu).",
506
507
  "Etiketin metni options'tan tazelenir: aynı value'lu option listede varsa onun label'ı yazılır. Bu arama METİNLEŞTİREREK karşılaştırır (String(a) === String(b)), AMA yalnız etiket metni için: satırın işaretli görünmesi ve seçimin kaldırılması KATI karşılaştırma kullanır. Yani value'da metin '34', options'ta sayı 34 varsa çip doğru etiketle çizilir ama liste satırı işaretsiz kalır ve tıklamak hiçbir şey yapmaz — eşlemeyi akış katmanında aynı tipe getir (CQ-208).",
507
508
  "change yükü { value: [{ value, label, id, text }] } taşır ve düğümün value prop'una yazılır: akışta self.value ile okunur. id/text, value/label'ın eski adlı ikizleridir — kayıtlı sayfalar onlarla okuduğu için bilerek korunuyor.",
@@ -578,7 +579,8 @@ export const componentKnowledge = {
578
579
  maxChars: "SERT sınır (tarayıcı maxlength): fazlası yazılamaz. Akıştan gelen daha uzun değer aynen gösterilir (sayaç 45/40 okur). validations.maxLength'ten FARKLIDIR: o yalnız doğrular. 0/boş = sınırsız.",
579
580
  showCounter: "Kutunun altında sağa dayalı sayaç: maxChars varsa '12/40', yoksa '12'.",
580
581
  autofocus: 'Sayfa açılınca odak bu alana gelir (dondurulmuş alanda ve tasarım kanvasında çalışmaz). Sayfada tek alana ver.',
581
- isLoading: "Alanı dondurur (readonly, gri değil) ve sağ üstte spinner çizer; changed'e bağlı sorgu sürerken aç, tamamlanınca kapat."
582
+ isLoading: "Alanı dondurur (readonly, gri değil) ve sağ üstte spinner çizer; changed'e bağlı sorgu sürerken aç, tamamlanınca kapat.",
583
+ selection: "selection { start, end } (f-07): kullanıcı seçince ya da imleci taşıyınca selectionChange yazar; yazarken kayan imleç yalnız prop'u günceller (akış koşmaz). Alana hiç dokunulmadıysa undefined'dır. Akış Components.<id>.selection'a yazarsa alan seçimi/imleci oraya koyar ve odaklanır (aynı akışta value da yazıldıysa yeni metne göre). Seçili parçayı biçimlemek ('Kalın' düğmesinin click akışı): const v = Components.<alan>.value ?? ''; const s = Components.<alan>.selection ?? { start: v.length, end: v.length }; const yeni = v.slice(0, s.start) + '**' + v.slice(s.start, s.end) + '**' + v.slice(s.end); const imlec = s.start === s.end ? s.start + 2 : s.end + 4; sonra setter ile value = yeni, selection = { start: imlec, end: imlec } (seçimde imleç kapanış işaretinden sonra, boş imleçte iki işaretin arasında). Web ve mobil aynı yükü verir; textinput'ta bu olay YOK."
582
584
  }
583
585
  },
584
586
  texteditor: {
@@ -125,7 +125,19 @@ export const componentEventContracts = {
125
125
  focus: NO_DATA,
126
126
  blur: NO_DATA
127
127
  },
128
- textarea: { mount: NO_DATA, change: valueChange('string'), changed: valueChange('string (blur ile kesinleşen değer)'), focus: NO_DATA, blur: NO_DATA },
128
+ textarea: {
129
+ mount: NO_DATA,
130
+ change: valueChange('string'),
131
+ changed: valueChange('string (blur ile kesinleşen değer)'),
132
+ focus: NO_DATA,
133
+ blur: NO_DATA,
134
+ // f-07 (plans/defter-saha-bulgulari): yük hem prop'a (`self.selection`) hem `params` ile `event.data`ya gider.
135
+ selectionChange: {
136
+ payload: {
137
+ selection: '{ start, end } — seçimin başı ve sonu, UTF-16 indeks (JS `slice` ile aynı birim); imleçte start === end. Aynı değer event.data.start / event.data.end olarak da gelir. Kullanıcı seçince ya da imleci taşıyınca yayılır (iki platformda 50 ms içindeki değişimler tek olay). Yazarken kayan imleç akış KOŞTURMAZ: yalnız self.selection güncellenir (her tuşta akış olmasın); biçim düğmesi seçimi tıklamada Components.<id>.selection\'dan okur'
138
+ }
139
+ }
140
+ },
129
141
  password: {
130
142
  mount: NO_DATA,
131
143
  change: valueChange('string'),
@@ -198,7 +198,10 @@ export const mobileComponentKnowledge = {
198
198
  differences: [
199
199
  WEB_WORDS_NOTE,
200
200
  'Yükseklik açıkça verilir (React Native `TextInput` satır sayısını kendi ölçmez): `autoGrow` (web varsayılanı AÇIK) iken içerikle `minLines`–`maxLines` arasında büyür, kapalıyken sabit `lines`. 2026-09-27\'ye dek hep sabit boydu (f-05). Satır yüksekliği `punto × 1,45` stile yazılır ve satır tabanlı boylar iç boşluk + kenarlığı içerir: `maxLines: 5` beş tam satır gösterir (2026-09-29). `autofocus` runtime\'da klavyeyi açar.'
201
- ]
201
+ ],
202
+ propNotes: {
203
+ selection: "Web ile aynı sözleşme (componentCatalog textarea.propNotes.selection). Cihazda seçim `onSelectionChange`'ten gelir ve 50 ms içinde tek `selectionChange`'e katlanır; akışın yazdığı seçim `setSelection` ile konur ve alan odaklanır (klavye açılır). Studio önizlemesinde (react-native-web) aynı yol `setSelectionRange`."
204
+ }
202
205
  },
203
206
  numberinput: {
204
207
  summary: 'Sayısal klavye açan alan; değer `Intl.NumberFormat` ile biçimlenir.',
@@ -1669,11 +1669,12 @@ const SCREEN_EVENT_NAMES = ['mount', 'destroy'];
1669
1669
  * koşmaz. Web her gezinmede sayfayı baştan kurar (`Runtime.svelte` focus'u tanımaz), layout'un focus'u
1670
1670
  * yoktur. Ayna: `ng-apps-ui-native` `SCREEN_LIFECYCLE_EVENTS` (parite: `test/page-meta.test.ts`).
1671
1671
  */
1672
- const MOBILE_PAGE_EVENT_NAMES = [...SCREEN_EVENT_NAMES, 'focus'];
1672
+ const MOBILE_PAGE_EVENT_NAMES = [...SCREEN_EVENT_NAMES, 'focus', 'offlineQueueChange'];
1673
1673
  const SCREEN_EVENT_HINTS = {
1674
1674
  mount: 'mount (yüklendiğinde)',
1675
1675
  destroy: 'destroy (ayrılırken; temizlik akışları için)',
1676
- focus: 'focus (ekran her göründüğünde — ilk açılış ve geri dönüş; listeyi yeniden okumak için)'
1676
+ focus: 'focus (ekran her göründüğünde — ilk açılış ve geri dönüş; listeyi yeniden okumak için)',
1677
+ offlineQueueChange: 'offlineQueueChange (servis yazma kuyruğu değişince — ağsız kayıt girdi, gönderildi, reddedildi; listeyi yeniden okumak için, sayaç Device.offline)'
1677
1678
  };
1678
1679
  export function screenEventNames(kind, platform) {
1679
1680
  return kind === 'page' && platform === 'mobile' ? MOBILE_PAGE_EVENT_NAMES : SCREEN_EVENT_NAMES;
@@ -1688,7 +1689,7 @@ export function invalidScreenEventNames(events, code, names = SCREEN_EVENT_NAMES
1688
1689
  .map(event => String(event.name ?? ''))
1689
1690
  .filter(name => name !== '' && !names.includes(name))
1690
1691
  .map(name => MOBILE_PAGE_EVENT_NAMES.includes(name)
1691
- ? `${code}: '${name}' yalnız mobil uygulamanın SAYFASINDA vardır (layout'ta ve web sayfasında yok; web sayfayı her gezinmede baştan kurar, mount yeter) — burada geçerli adlar: ${valid}.`
1692
+ ? `${code}: '${name}' yalnız mobil uygulamanın SAYFASINDA vardır (layout'ta ve web sayfasında yok; ${name === 'offlineQueueChange' ? 'web barındırıcısı servis yazma kuyruğu tutmaz, olay hiç yayılmaz' : 'web sayfayı her gezinmede baştan kurar, mount yeter'}) — burada geçerli adlar: ${valid}.`
1692
1693
  : `${code}: sayfa/layout düzeyinde '${name}' diye bir olay YOK — geçerli adlar: ${valid}. Platform kaydı kabul eder ama bağ sessizce ölü kalır.`);
1693
1694
  }
1694
1695
  const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -100,6 +100,14 @@ export async function applyArgumentAliases(tool, args, context) {
100
100
  * harf duyarsız düzenleme uzaklığı 2'yi geçmeyen en yakın ad (`hostID` → `hostId`, `views` → `view`).
101
101
  */
102
102
  export function closestArgument(key, accepted) {
103
+ return closestNames(key, accepted, 1, 2)[0];
104
+ }
105
+ /**
106
+ * `key`'e en yakın `limit` ad (büyük/küçük harf duyarsız düzenleme uzaklığı, eşitlikte `accepted` sırası). `maxDistance`
107
+ * verilmezse uzaklık sınırı yoktur: g-21'de micro-op'un tanınmayan prop'una komponentin en yakın üç prop'u önerilir
108
+ * (`ariaLabel` → `label`), o adlar 2'den uzak olsa da sözlüğün nereye baktığını gösterir.
109
+ */
110
+ export function closestNames(key, accepted, limit, maxDistance = Number.POSITIVE_INFINITY) {
103
111
  const distance = (left, right) => {
104
112
  const row = Array.from({ length: right.length + 1 }, (_, index) => index);
105
113
  for (let i = 1; i <= left.length; i++) {
@@ -113,11 +121,12 @@ export function closestArgument(key, accepted) {
113
121
  }
114
122
  return row[right.length] ?? 0;
115
123
  };
116
- const ranked = accepted
124
+ return accepted
117
125
  .map(name => ({ name, score: distance(key.toLowerCase(), name.toLowerCase()) }))
118
- .filter(entry => entry.score <= 2)
119
- .sort((left, right) => left.score - right.score);
120
- return ranked[0]?.name;
126
+ .filter(entry => entry.score <= maxDistance)
127
+ .sort((left, right) => left.score - right.score)
128
+ .slice(0, limit)
129
+ .map(entry => entry.name);
121
130
  }
122
131
  const activeNotes = new AsyncLocalStorage();
123
132
  export function runWithArgumentNotes(notes, run) {
@@ -1,6 +1,6 @@
1
- import { open, realpath } from 'node:fs/promises';
1
+ import { open, realpath, stat } from 'node:fs/promises';
2
2
  import { constants } from 'node:fs';
3
- import { extname, resolve, sep } from 'node:path';
3
+ import { dirname, extname, isAbsolute, resolve, sep } from 'node:path';
4
4
  import { z } from 'zod';
5
5
  import { PlatformApiError } from './errors.js';
6
6
  /**
@@ -27,77 +27,234 @@ export const BODY_FILE_ARGUMENTS = {
27
27
  /** `ops`/`operations` eşanlamlıdır (`microOps`); ikisinden biri satır içinde varsa gövde verilmiştir. */
28
28
  const synonyms = { ops: ['ops', 'operations'], operations: ['ops', 'operations'] };
29
29
  const maxBodyFileBytes = 5 * 1024 * 1024;
30
+ /**
31
+ * g-22 (D-22, K8 — `plans/defter-saha-bulgulari`): akış kodu JSON metninin içinde kaçışlı dizgi olarak duruyordu;
32
+ * Defter'de bir ters tırnak hatası düzenleri kırdı ve iş JS üreteçlerine taşındı. `{ "$file": "./adim.js" }` o
33
+ * dosyanın METNİYLE değiştirilir — gateway kodu ÇALIŞTIRMAZ (reddedilen seçenek: gateway'de .js koşturmak). Eklenen
34
+ * dosya bodyFile'ın bütün denetimlerinden geçer; sınırlar sahibin onayladığı K8 değerleridir.
35
+ */
36
+ const includedExtensions = ['.js', '.mjs', '.ts', '.txt', '.sql', '.html', '.css'];
37
+ const maxIncludedFileBytes = 1024 * 1024;
38
+ const maxInclusions = 64;
30
39
  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.`);
40
+ 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. Göreli yol .env'deki MCP_INPUT_DIR'e (tanımsızsa gateway'in çalışma dizinine) göre çözülür; dosya bu iki dizinden birinin altında olmalı (sembolik bağ çözülür). JSON'un herhangi bir yerine { "$file": "./adim.js" } yazılırsa o dosyanın METNİ oraya konur — kod çalıştırılmaz; yol bodyFile'ın klasörüne göre çözülür; ${includedExtensions.join(' ')}, UTF-8, dosya başına 1 MB, toplam 5 MB, en çok ${maxInclusions} ekleme; eklenen dosyanın içindeki $file işlenmez. Biçim tam { "$file": "<yol>" }'dur (tek anahtar, dizgi) — bozuk biçim ve satır içi gövdedeki $file istek gitmeden reddedilir. Akış code adımının args.value'su böylece kaçışsız düz JS dosyasında durur. Yanıtın bodyFile alanı okunan dosyanın mutlak yolunu, kökünü (root, rootSource) ve eklenen dosyaları (included: at, path, bytes) söyler.`);
32
41
  }
33
42
  export async function loadBodyFile(tool, args, config) {
34
43
  const argument = BODY_FILE_ARGUMENTS[tool];
35
44
  if (!argument || !args || typeof args !== 'object' || Array.isArray(args))
36
45
  return { args };
37
46
  const { bodyFile, ...rest } = args;
38
- if (bodyFile === undefined)
39
- return { args };
40
- if (typeof bodyFile !== 'string' || bodyFile.length === 0)
41
- return { args };
42
47
  const inline = (synonyms[argument] ?? [argument]).filter(name => rest[name] !== undefined);
48
+ if (typeof bodyFile !== 'string' || bodyFile.length === 0) {
49
+ assertNoInlineInclusions(tool, inline, rest);
50
+ return { args };
51
+ }
43
52
  if (inline.length > 0) {
44
53
  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
54
  }
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);
55
+ // g-21 (D-21): göreli yol gateway sürecinin çalışma dizinine göre çözülüyordu ve ajan o dizini bilmez. Ajanın
56
+ // bildiği kök .env'deki MCP_INPUT_DIR'dir; tanımsızsa eski davranış (çalışma dizini) sürer.
57
+ const base = config.inputDirectory ? { source: 'MCP_INPUT_DIR', path: resolve(config.inputDirectory) } : { source: 'cwd', path: process.cwd() };
58
+ const roots = await allowedRoots(config);
59
+ const body = await readGuardedFile(tool, 'bodyFile', resolve(base.path, bodyFile), ['.json'], maxBodyFileBytes, roots, config);
60
+ let value;
61
+ try {
62
+ value = JSON.parse(body.buffer.toString('utf8'));
63
+ }
64
+ catch (error) {
65
+ throw new PlatformApiError(`${tool}: bodyFile JSON değil — ${body.real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
66
+ }
67
+ const included = await includeFiles(tool, value, dirname(body.real), body.buffer.length, roots, config);
68
+ const root = isAbsolute(bodyFile) ? body.root : { source: base.source, path: await realpath(base.path).catch(() => base.path) };
69
+ return {
70
+ args: { ...rest, [argument]: included.value },
71
+ note: {
72
+ path: body.real,
73
+ root: root.path,
74
+ rootSource: root.source,
75
+ argument,
76
+ bytes: body.buffer.length,
77
+ ...(included.files.length > 0 ? { included: included.files } : {})
78
+ }
79
+ };
80
+ }
81
+ /** MCP_INPUT_DIR önce gelir: dosya iki kökün de altındaysa yanıt ajanın bildiği kökü söyler. */
82
+ async function allowedRoots(config) {
83
+ const candidates = [
84
+ ...(config.inputDirectory ? [{ source: 'MCP_INPUT_DIR', path: config.inputDirectory }] : []),
85
+ { source: 'cwd', path: process.cwd() }
86
+ ];
87
+ const resolved = await Promise.all(candidates.map(async (root) => ({ source: root.source, path: await realpath(resolve(root.path)).catch(() => undefined) })));
88
+ return resolved.filter((root) => root.path !== undefined);
89
+ }
90
+ /**
91
+ * bodyFile'ın ve her `$file` eklemesinin ORTAK okuma kapısı: uzantı (yol ve bağın hedefi), izinli kök, düzenli dosya
92
+ * ve bayt sınırı. O_NONBLOCK FIFO'nun çağrıyı bekletmesini önler; boyut dosyanın büyüklüğüne değil okunan bayta bakar.
93
+ *
94
+ * `realpath` ile `open` atomik DEĞİLDİR: O_NOFOLLOW yalnız SON bileşenin sonradan bağa çevrilmesini önler, yolun
95
+ * ortasındaki bir klasör denetimden sonra dışarıyı gösteren bağa çevrilirse `open` dışarıdaki dosyayı açar (g-22
96
+ * incelemesi, `test/body-file-race.test.ts`). Açtıktan sonra tutamacın aygıt/düğümü yolun taze `stat`'ıyla
97
+ * karşılaştırılır ve `realpath` yeniden koşturulur: yol aynı yere çözülmüyor ya da izinli kökün dışına düşüyorsa okuma
98
+ * reddedilir. Bu pencereyi DARALTIR, kapatmaz — iki denetim arasında geri çevrilen bir bağ yine görülmez; tam çözüm
99
+ * (`openat2` RESOLVE_BENEATH) Node'da yoktur. Okunan bayt yine de açılan tutamaçtan gelir.
100
+ */
101
+ async function readGuardedFile(tool, label, absolute, extensions, maxBytes, roots, config) {
102
+ const accepted = extensions.join(', ');
103
+ if (!extensions.includes(extname(absolute).toLowerCase())) {
104
+ throw new PlatformApiError(`${tool}: ${label} yalnız ${accepted} okur — verilen: ${absolute}. İstek gönderilmedi.`, 400);
49
105
  }
50
106
  let real;
51
107
  try {
52
108
  real = await realpath(absolute);
53
109
  }
54
110
  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);
111
+ throw new PlatformApiError(`${tool}: ${label} okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür. İstek gönderilmedi.`, 400);
56
112
  }
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);
113
+ if (!extensions.includes(extname(real).toLowerCase())) {
114
+ throw new PlatformApiError(`${tool}: ${label} hedefi ${accepted} değil — ${real} (${absolute} bağının hedefi). İstek gönderilmedi.`, 400);
59
115
  }
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);
116
+ const root = roots.find(candidate => real === candidate.path || real.startsWith(`${candidate.path}${sep}`));
117
+ if (!root) {
118
+ throw new PlatformApiError(`${tool}: ${label} 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
119
  }
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
120
  let handle;
71
121
  try {
72
122
  handle = await open(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
73
123
  }
74
124
  catch (error) {
75
- throw new PlatformApiError(`${tool}: bodyFile açılamadı — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
125
+ throw new PlatformApiError(`${tool}: ${label} açılamadı — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
76
126
  }
77
- let bytes;
78
- let text;
79
127
  try {
80
128
  const info = await handle.stat();
81
129
  if (!info.isFile()) {
82
- throw new PlatformApiError(`${tool}: bodyFile düzenli dosya değil — ${real}. İstek gönderilmedi.`, 400);
130
+ throw new PlatformApiError(`${tool}: ${label} düzenli dosya değil — ${real}. İstek gönderilmedi.`, 400);
131
+ }
132
+ const fresh = await stat(real).catch(() => undefined);
133
+ const again = await realpath(absolute).catch(() => undefined);
134
+ const stillInside = again !== undefined && (again === root.path || again.startsWith(`${root.path}${sep}`));
135
+ if (!fresh || fresh.dev !== info.dev || fresh.ino !== info.ino || again !== real || !stillInside) {
136
+ throw new PlatformApiError(`${tool}: ${label} yolu denetimden sonra değişti — ${real} artık ${again ?? 'çözülemeyen bir yere'} gösteriyor (araya bir bağ girdi). Okunmadı. İstek gönderilmedi.`, 400);
83
137
  }
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);
138
+ const buffer = Buffer.alloc(maxBytes + 1);
139
+ let bytesRead = 0;
140
+ // Tek `read` dosyanın tamamını döndürmek zorunda değildir; EOF'a ya da sınırın bir baytı ötesine kadar okunur.
141
+ while (bytesRead < buffer.length) {
142
+ const chunk = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
143
+ if (chunk.bytesRead === 0)
144
+ break;
145
+ bytesRead += chunk.bytesRead;
89
146
  }
90
- text = buffer.subarray(0, bytes).toString('utf8');
147
+ if (bytesRead > maxBytes) {
148
+ throw new PlatformApiError(maxBytes === maxBodyFileBytes
149
+ ? `${tool}: ${label} 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.`
150
+ : `${tool}: ${label} ${maxBytes / (1024 * 1024)} MB sınırını aşıyor — ${real}. Kodu birden çok dosyaya bölün. İstek gönderilmedi.`, 400);
151
+ }
152
+ return { real, root, buffer: buffer.subarray(0, bytesRead) };
91
153
  }
92
154
  finally {
93
155
  await handle.close();
94
156
  }
95
- let value;
96
- try {
97
- value = JSON.parse(text);
157
+ }
158
+ /** Kök JSON Pointer'ı boş dizgidir; mesajda `''` ile `/` (adı boş anahtar) karışmasın diye adıyla yazılır. */
159
+ function pointerLabel(at) {
160
+ return at === '' ? '(kök)' : at;
161
+ }
162
+ function pointerToken(key) {
163
+ return String(key).replace(/~/g, '~0').replace(/\//g, '~1');
164
+ }
165
+ /**
166
+ * Anahtarlarından biri büyük/küçük harf duyarsız `$file` olan her nesne ekleme NİYETİDİR. Yalnız tam
167
+ * `{ "$file": "<yol>" }` biçimi eklemedir; dizi/sayı değer, ek anahtar ya da `$File` yazımı sessizce nesne olarak
168
+ * platforma gitmesin diye `malformed`'a düşer (g-22 incelemesi 2).
169
+ */
170
+ function findInclusions(value, at, found, malformed) {
171
+ if (Array.isArray(value)) {
172
+ value.forEach((item, index) => findInclusions(item, `${at}/${index}`, found, malformed));
173
+ return;
98
174
  }
99
- catch (error) {
100
- throw new PlatformApiError(`${tool}: bodyFile JSON değil — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
175
+ if (!value || typeof value !== 'object')
176
+ return;
177
+ const record = value;
178
+ const keys = Object.keys(record);
179
+ const marker = keys.find(key => key.toLowerCase() === '$file');
180
+ if (marker !== undefined) {
181
+ if (keys.length === 1 && marker === '$file' && typeof record.$file === 'string') {
182
+ found.push({ at, file: record.$file });
183
+ }
184
+ else {
185
+ const problem = marker !== '$file'
186
+ ? `anahtar '${marker}' yazılmış, '$file' olmalı`
187
+ : keys.length > 1
188
+ ? `'$file' yanında başka anahtar var (${keys.filter(key => key !== '$file').join(', ')})`
189
+ : `'$file' değeri dizgi değil (${JSON.stringify(record[marker])})`;
190
+ malformed.push(`${pointerLabel(at)}: ${problem}`);
191
+ }
192
+ return;
193
+ }
194
+ for (const [key, item] of Object.entries(record))
195
+ findInclusions(item, `${at}/${pointerToken(key)}`, found, malformed);
196
+ }
197
+ /**
198
+ * Satır içi gövdede `$file` açılmaz: dosya okuma yalnız bodyFile yolunda vardır. Açılmayan nesne platforma nesne
199
+ * olarak giderdi; istek gitmeden söylenir. `$file` anahtarı yoksa yalnız bir ağaç yürüyüşüdür, okuma yapılmaz.
200
+ */
201
+ function assertNoInlineInclusions(tool, names, args) {
202
+ for (const name of names) {
203
+ const found = [];
204
+ const malformed = [];
205
+ findInclusions(args[name], '', found, malformed);
206
+ const sites = [...found.map(site => pointerLabel(site.at)), ...malformed];
207
+ if (sites.length > 0) {
208
+ throw new PlatformApiError(`${tool}: '${name}' içinde $file nesnesi var (${sites.join(' · ')}) — $file yalnız bodyFile gövdesinde açılır. Metni satır içinde doğrudan yazın ya da gövdeyi bodyFile ile (.json dosyası) verin. İstek gönderilmedi.`, 400);
209
+ }
210
+ }
211
+ }
212
+ function replaceInclusions(value, at, texts) {
213
+ const text = texts.get(at);
214
+ if (text !== undefined)
215
+ return text;
216
+ if (Array.isArray(value))
217
+ return value.map((item, index) => replaceInclusions(item, `${at}/${index}`, texts));
218
+ if (value && typeof value === 'object') {
219
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replaceInclusions(item, `${at}/${pointerToken(key)}`, texts)]));
220
+ }
221
+ return value;
222
+ }
223
+ /**
224
+ * Eklenen metin bir kez okunur ve olduğu gibi konur: satır sonu korunur, metin yeniden JSON olarak ayrıştırılmaz —
225
+ * eklenen dosyanın içindeki `$file` bu yüzden metin kalır (iç içe ekleme yok). UTF-8 olmayan dosya sessizce � ile
226
+ * bozulacağına reddedilir; baştaki UTF-8 BOM'u çözücü düşürür (`bytes` onu yine sayar). Yer bilgisi (`at`) gövde argümanına göre JSON Pointer'dır.
227
+ */
228
+ async function includeFiles(tool, value, folder, bodyBytes, roots, config) {
229
+ const sites = [];
230
+ const malformed = [];
231
+ findInclusions(value, '', sites, malformed);
232
+ if (malformed.length > 0) {
233
+ throw new PlatformApiError(`${tool}: bodyFile'da biçimi bozuk $file nesnesi — ${malformed.join(' · ')}. Ekleme yalnız tam { "$file": "<yol>" } biçimidir (tek anahtar, dizgi değer). İstek gönderilmedi.`, 400);
234
+ }
235
+ if (sites.length === 0)
236
+ return { value, files: [] };
237
+ if (sites.length > maxInclusions) {
238
+ throw new PlatformApiError(`${tool}: bodyFile ${sites.length} $file eklemesi taşıyor; sınır ${maxInclusions}. İlk fazlası: ${pointerLabel(sites[maxInclusions]?.at ?? '')} — gövdeyi birden çok çağrıya bölün. İstek gönderilmedi.`, 400);
239
+ }
240
+ const decoder = new TextDecoder('utf-8', { fatal: true });
241
+ const texts = new Map();
242
+ const files = [];
243
+ let total = bodyBytes;
244
+ for (const site of sites) {
245
+ const label = `$file (${pointerLabel(site.at)} → '${site.file}')`;
246
+ const read = await readGuardedFile(tool, label, resolve(folder, site.file), includedExtensions, maxIncludedFileBytes, roots, config);
247
+ total += read.buffer.length;
248
+ if (total > maxBodyFileBytes) {
249
+ throw new PlatformApiError(`${tool}: bodyFile ve $file eklemeleri birlikte 5 MB toplam gövde sınırını aşıyor (${pointerLabel(site.at)} eklemesinde). Tanımı parçalara bölün. İstek gönderilmedi.`, 400);
250
+ }
251
+ try {
252
+ texts.set(site.at, decoder.decode(read.buffer));
253
+ }
254
+ catch {
255
+ throw new PlatformApiError(`${tool}: ${label} UTF-8 değil — ${read.real}. Dosyayı UTF-8 olarak kaydedin. İstek gönderilmedi.`, 400);
256
+ }
257
+ files.push({ at: site.at, path: read.real, bytes: read.buffer.length });
101
258
  }
102
- return { args: { ...rest, [argument]: value }, note: `${bodyFile} → ${argument} (${bytes} B)` };
259
+ return { value: replaceInclusions(value, '', texts), files };
103
260
  }
@@ -1,4 +1,5 @@
1
1
  import { INTERNAL_ROLE_SEPARATOR } from '../../knowledge/componentCatalog.js';
2
+ import { AWAITING_WINDOW_SELECTOR } from './readiness.js';
2
3
  import { studioWebTarget } from './targets/studio-web.js';
3
4
  /**
4
5
  * `swipe` jestinin adım sayısı ve aralığı: react-native-web yanıtlayıcısı tek seferde atılan hareketi jest saymaz
@@ -38,12 +39,23 @@ export function actionScript(action, captureTarget = studioWebTarget) {
38
39
  return `(function () {
39
40
  var count = performance.getEntriesByType('resource').length;
40
41
  var node = document.querySelector(${selector});
41
- if (!node) return { found: false, applied: false, requests: count };
42
+ if (!node) {
43
+ // g-11: açık önizleme penceresi (onay/uyarı/biyometri) sayfanın düğmesini örtüyor olabilir; not hangisi açık söyler.
44
+ var open = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
45
+ return { found: false, applied: false, requests: count, openWindow: open ? { id: String(open.getAttribute('data-element-id')), kind: open.getAttribute('data-dialog-kind') } : null };
46
+ }
42
47
  // g-04 sırası: düğümün kendisi basılabilirse o → <id>__press → içteki ilk basılabilir öğe.
43
48
  // Mobil çoklu seçim tetikleyicisi kökte role="combobox" taşır ve içindeki ilk role="button"
44
49
  // çipin "kaldır" düğmesidir — eskiden ona basılıp seçim siliniyordu (37 S-18). Tek basışlı
45
50
  // denetimin yüzeyi <id>__press'tir (ui-native press-role-guard), anahtarınki role="switch".
46
51
  var press = document.querySelector(${JSON.stringify(`[data-element-id="${String(action.target ?? '')}${INTERNAL_ROLE_SEPARATOR}press"]`)});
52
+ // g-11 incelemesi: önizleme penceresi (onay/uyarı/biyometri) açıkken altında kalan sayfa öğesine gerçek kullanıcı
53
+ // basamaz; DOM tıklaması basardı ve sayfa eylemi pencere yanıtlanmadan koşardı.
54
+ var shield = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
55
+ if (shield && !shield.contains(node)) {
56
+ var shieldId = String(shield.getAttribute('data-element-id'));
57
+ return { found: true, applied: false, requests: count, openWindow: { id: shieldId, kind: shield.getAttribute('data-dialog-kind') }, note: "önizleme penceresi " + shieldId + " açık ve sayfayı örtüyor: gerçek kullanıcı alttaki öğeye basamaz, eylem uygulanmadı — önce pencereyi yanıtlayın (" + shieldId + "__confirm / __cancel, biyometride __verify / __cancel)" };
58
+ }
47
59
  var target = ${pressSurface} || node.matches('button, a, [role="button"], [role="switch"], [role="combobox"]') ? node
48
60
  : (press || node.querySelector('button, a, [role="button"], [role="switch"]') || node);
49
61
  // S-2: sekme grubunun KÖKÜNE click, DOM sırasındaki ilk sekmeye gider ve "uygulandı" der —
@@ -433,6 +445,13 @@ export function actionScript(action, captureTarget = studioWebTarget) {
433
445
  // K1 (g-04): gerçek kullanıcı alana odaklanır, yazar ve çıkar. Blur'suz yazımda odak kaybına
434
446
  // bağlı akış (ui-native girişlerin 'changed'i) hiç koşmuyordu (37 S-19). focus() odağı alamazsa
435
447
  // (görünmez alan, odaksız belge) olaylar elle atılır: Svelte 'blur'u, React 'focusout'u dinler.
448
+ // blur: false (g-12) telefonda klavye açıkken yazılan hâli sınar: odak alanda kalır, not söyler.
449
+ var keepFocus = ${JSON.stringify(action.blur === false)};
450
+ var done = function () {
451
+ var result = { found: true, applied: true, requests: count };
452
+ if (keepFocus) result.note = "blur: false — odak alanda kaldı; odak kaybına bağlı akış ('changed') bu adımda koşmaz";
453
+ return finish(result);
454
+ };
436
455
  var focusField = function () {
437
456
  field.focus();
438
457
  if (document.activeElement === field) return;
@@ -448,15 +467,15 @@ export function actionScript(action, captureTarget = studioWebTarget) {
448
467
  if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
449
468
  var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
450
469
  if (field.checked !== shouldCheck) field.click();
451
- blurField();
452
- return finish({ found: true, applied: true, requests: count });
470
+ if (!keepFocus) blurField();
471
+ return done();
453
472
  }
454
473
  var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
455
474
  setter.call(field, ${value});
456
475
  field.dispatchEvent(new Event('input', { bubbles: true }));
457
476
  field.dispatchEvent(new Event('change', { bubbles: true }));
458
- blurField();
459
- return finish({ found: true, applied: true, requests: count });
477
+ if (!keepFocus) blurField();
478
+ return done();
460
479
  })()`;
461
480
  }
462
481
  /** `rf_range__start` → `rf_range`: iç rol hedefinin prop'ları kök düğümde yaşar. */