@appaflytech/zeyrek-ai-mcp 0.0.3 → 0.0.4

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 (35) hide show
  1. package/.env.example +21 -7
  2. package/.env.production.example +42 -0
  3. package/README.md +14 -7
  4. package/dist/config.js +113 -47
  5. package/dist/knowledge/componentCatalog.js +474 -31
  6. package/dist/knowledge/componentEvents.js +150 -12
  7. package/dist/knowledge/errorPath.js +64 -0
  8. package/dist/knowledge/normalize.js +12 -1
  9. package/dist/knowledge/outlines.js +6 -0
  10. package/dist/knowledge/rules.js +184 -3
  11. package/dist/knowledge/styleResolve.js +485 -0
  12. package/dist/observability/trace.js +4 -0
  13. package/dist/platform/http.js +3 -0
  14. package/dist/platform/responses.js +80 -4
  15. package/dist/platform/screenshot.js +106 -0
  16. package/dist/resources/examples/layout-app-shell.json +368 -0
  17. package/dist/resources/examples/microservice-table-list.json +1 -0
  18. package/dist/resources/examples/middleware-authenticate.json +243 -47
  19. package/dist/resources/examples/workflow-basic.json +24 -2
  20. package/dist/resources/guides/component.md +6 -3
  21. package/dist/resources/guides/external-component.md +129 -0
  22. package/dist/resources/guides/layout.md +147 -0
  23. package/dist/resources/guides/microservice.md +35 -0
  24. package/dist/resources/guides/middleware.md +10 -0
  25. package/dist/resources/guides/page.md +132 -14
  26. package/dist/resources/guides/workflow.md +127 -2
  27. package/dist/server.js +2 -0
  28. package/dist/tools/externalComponents.js +285 -0
  29. package/dist/tools/layouts.js +48 -6
  30. package/dist/tools/microservices.js +2 -2
  31. package/dist/tools/pages.js +98 -30
  32. package/dist/tools/platformInfo.js +2 -2
  33. package/dist/tools/screens.js +36 -0
  34. package/dist/tools/workflows.js +3 -3
  35. package/package.json +4 -1
package/dist/server.js CHANGED
@@ -5,6 +5,7 @@ import { registerDeploymentTools } from './tools/deployments.js';
5
5
  import { registerDatasourceTools } from './tools/datasources.js';
6
6
  import { registerAutomationTools } from './tools/automations.js';
7
7
  import { registerOrganizationTools } from './tools/organizations.js';
8
+ import { registerExternalComponentTools } from './tools/externalComponents.js';
8
9
  import { registerComponentTools } from './tools/components.js';
9
10
  import { registerPlatformTools } from './tools/platformInfo.js';
10
11
  import { registerTemplateTools } from './tools/templates.js';
@@ -42,6 +43,7 @@ const moduleRegistrations = {
42
43
  view: registerViewTools,
43
44
  page: registerPageTools,
44
45
  component: registerComponentTools,
46
+ externalComponent: registerExternalComponentTools,
45
47
  flow: registerFlowTools,
46
48
  layout: registerLayoutTools,
47
49
  variable: registerVariableTools,
@@ -0,0 +1,285 @@
1
+ import { guid, projectParam, environmentParam, readOnly, writes, updates, destructive } from './common.js';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { PlatformApiError } from '../platform/errors.js';
4
+ import { handle } from '../platform/responses.js';
5
+ import { basename, join, resolve } from 'node:path';
6
+ import { z } from 'zod';
7
+ /** Platformun köprü protokolü; scaffold ve publish bu sürüme göre üretir/doğrular */
8
+ const PROTOCOL_VERSION = 1;
9
+ /** Backend `ExternalComponentRules.MaxBundleSizeBytes` ile aynı sınır */
10
+ const maxBundleBytes = 2 * 1024 * 1024;
11
+ const slugPattern = /^x-[a-z][a-z0-9-]*$/;
12
+ const slugParam = z.string().regex(slugPattern)
13
+ .describe("Harici komponent slug'ı; 'x-' ile başlamak zorunda (örn. 'x-rating-stars') — bu önek platform slug'larıyla çakışmayı imkânsız kılar");
14
+ const versionParam = z.string().min(1).describe("Paket sürümü, örn. '1.0.0'; aynı sürüm ikinci kez yayınlanamaz");
15
+ /**
16
+ * Doğrulama raporunu ajanın düzeltme döngüsü kurabileceği şekilde özetler: block
17
+ * bulguları işi durdurur, warn bulguları aktivasyonda açık onay ister.
18
+ */
19
+ function summarizeFindings(findings) {
20
+ const blocking = (findings ?? []).filter(finding => finding.severity === 'block');
21
+ const warnings = (findings ?? []).filter(finding => finding.severity === 'warn');
22
+ return {
23
+ blocking,
24
+ warnings,
25
+ nextStep: blocking.length
26
+ ? 'Paket reddedildi ve depolamaya yazılmadı. Yukarıdaki block bulgularını kaynakta düzeltip yeniden yayınlayın.'
27
+ : warnings.length
28
+ ? "Paket taslak olarak kaydedildi. Uyarıları inceleyin; aktive etmek için external_component_activate'i acknowledgeWarnings:true ile çağırın."
29
+ : "Paket taslak olarak kaydedildi. Önizlemede doğruladıktan sonra external_component_activate ile yayına alın."
30
+ };
31
+ }
32
+ const elementTemplate = (slug, props) => `const PROPS = ${JSON.stringify(props)}
33
+
34
+ export default class ZeyrekElement extends HTMLElement {
35
+ #props = {}
36
+ #context = null
37
+ #machine = null
38
+
39
+ ${props.map(prop => ` set ${prop}(value) {
40
+ this.#props.${prop} = value
41
+ this.#render()
42
+ }`).join('\n\n')}
43
+
44
+ set zxContext(value) {
45
+ this.#context = value
46
+ this.#render()
47
+ }
48
+
49
+ set zxMachine(value) {
50
+ this.#machine = value
51
+ }
52
+
53
+ connectedCallback() {
54
+ this.#render()
55
+ }
56
+
57
+ /**
58
+ * Platforma bir olay bildirir. setter:true, data içindeki değerin aynı adlı
59
+ * prop'a geri yazılmasını ister — value gibi iki yönlü davranan proplar için.
60
+ */
61
+ emit(name, data, options = {}) {
62
+ this.dispatchEvent(new CustomEvent('zx-emit', { detail: { name, data, ...options } }))
63
+ }
64
+
65
+ #render() {
66
+ if (this.#context?.editorMode) {
67
+ this.textContent = '${slug}'
68
+ return
69
+ }
70
+
71
+ this.textContent = PROPS.map(prop => \`\${prop}: \${this.#props[prop] ?? ''}\`).join(' · ')
72
+ }
73
+ }
74
+ `;
75
+ const manifestTemplate = (slug, name, props) => JSON.stringify({
76
+ slug,
77
+ name,
78
+ description: `${name} — organizasyon tarafından yayınlanan komponent.`,
79
+ icon: 'widgets',
80
+ categories: ['common'],
81
+ protocolVersion: PROTOCOL_VERSION,
82
+ definition: {
83
+ props: props.map(prop => ({ title: prop, name: prop, type: 'text' })),
84
+ events: [{ name: 'change', title: 'When value changed' }],
85
+ validations: [],
86
+ blocks: [{ breakpoint: 1024, size: { w: 3, h: 8 }, minSize: { w: 1, h: 1 }, maxSize: { w: 12, h: 5 } }],
87
+ defaults: {}
88
+ }
89
+ }, null, 2);
90
+ const readmeTemplate = (slug) => `# ${slug}
91
+
92
+ Zeyrek harici komponenti. Köprü protokolü sürümü: ${PROTOCOL_VERSION}.
93
+
94
+ ## Sözleşme
95
+
96
+ Host, element'e şema prop'larını **JS property** olarak yazar (attribute değil —
97
+ dizi ve nesneler bozulmadan geçsin diye), yanına iki kanal koyar:
98
+
99
+ - \`zxContext\`: \`{ elementId, appearance, states, isRequired, editorMode, language }\`
100
+ - \`zxMachine\`: platform yüzeyi (state, değişken, dil, yetki, sibling prop'lar)
101
+
102
+ Element geriye tek bir DOM olayıyla konuşur: \`zx-emit\`, detail'i
103
+ \`{ name, data?, setter?, flowId?, params? }\`. \`setter: true\` verirseniz
104
+ \`data\` içindeki değer aynı adlı prop'a geri yazılır.
105
+
106
+ ## Kurallar
107
+
108
+ - Tek \`export default\`, \`HTMLElement\` türeten bir sınıf olmalı.
109
+ - \`eval\`, \`new Function\`, dinamik \`import()\`, \`document.cookie\`,
110
+ \`window.machine\`, service worker kaydı **yasak** — yayınlama reddedilir.
111
+ - Ağ erişimi, doğrudan storage ve \`innerHTML\` ataması uyarı üretir; aktive
112
+ ederken açık onay ister.
113
+ - Renk, boşluk ve tipografi için \`--ng-*\` tema değişkenlerini kullanın; sabit
114
+ renk yazmayın.
115
+
116
+ ## Yayınlama
117
+
118
+ 1. \`manifest.json\` içindeki prop ve event tanımlarını gerçek komponentinize göre düzenleyin.
119
+ 2. \`external_component_publish\` — kaydı ve paketi yükler, doğrulama raporunu döner.
120
+ 3. \`external_component_activate\` — taslağı yayına alır.
121
+ `;
122
+ export function registerExternalComponentTools(server, platform) {
123
+ const { config } = platform;
124
+ server.registerTool('external_component_list', {
125
+ title: 'Yayınlanan komponentleri listele',
126
+ description: "Organizasyonun kendi yayınladığı komponentleri (Studio'da 'Your Components' başlığı altında görünenler) listeler: slug, sürüm, taslak mı yayında mı. Platformun hazır komponentleri için component_list kullanın.",
127
+ inputSchema: {
128
+ project: projectParam,
129
+ environment: environmentParam,
130
+ applicationId: guid
131
+ },
132
+ annotations: readOnly
133
+ }, handle(config, async ({ project, environment, applicationId }) => {
134
+ const response = await platform.tenant('GET', project, `/components/${applicationId}`, {
135
+ environment,
136
+ query: { pageIndex: 0, pageLength: 999 }
137
+ });
138
+ const components = (response?.data ?? []).filter(component => component.origin === 'external');
139
+ return {
140
+ components: components.map(component => ({
141
+ id: component.id,
142
+ slug: component.slug,
143
+ name: component.name,
144
+ bundleVersion: component.bundleVersion ?? null,
145
+ bundleStatus: component.bundleStatus ?? null,
146
+ protocolVersion: component.protocolVersion ?? null
147
+ })),
148
+ note: components.length
149
+ ? undefined
150
+ : "Bu uygulamada yayınlanmış komponent yok. İhtiyacınızı karşılayan hazır bir komponent bulamadıysanız external_component_scaffold ile kendinizinkini yazmaya başlayabilirsiniz."
151
+ };
152
+ }));
153
+ server.registerTool('external_component_scaffold', {
154
+ title: 'Harici komponent iskeleti üret',
155
+ description: "Yerel bir dizine çalışan bir harici komponent iskeleti yazar: element.mjs (köprü protokolünü karşılayan sınıf), manifest.json (prop/event/boyut tanımı) ve README. Mevcut katalog ihtiyacınızı karşılamıyorsa buradan başlayın; ürettiği dosyaları düzenleyip external_component_publish ile yayınlarsınız. Dosyalar gateway sürecinin çalıştığı makineye yazılır, içerikleri model bağlamına girmez.",
156
+ inputSchema: {
157
+ directory: z.string().min(1).describe('İskeletin yazılacağı yerel dizin; yoksa oluşturulur'),
158
+ slug: slugParam,
159
+ name: z.string().min(1).describe("Studio toolbox'ta görünecek ad"),
160
+ props: z.array(z.string().min(1)).optional().describe("Komponentin alacağı prop adları; verilmezse ['value'] varsayılır. id/class/style/name gibi DOM adları kullanılamaz")
161
+ },
162
+ annotations: writes
163
+ }, handle(config, async ({ directory, slug, name, props }) => {
164
+ const propNames = props?.length ? props : ['value'];
165
+ const reserved = propNames.filter(prop => /^(id|class|className|style|slot|name|children|innerHTML|outerHTML|textContent)$/.test(prop) || prop.startsWith('zx') || prop.startsWith('on'));
166
+ if (reserved.length) {
167
+ throw new PlatformApiError(`external_component_scaffold: şu prop adları platform tarafından rezerve edilmiş ve yayınlama sırasında reddedilir: ${reserved.join(', ')}.`);
168
+ }
169
+ const target = resolve(directory);
170
+ await mkdir(target, { recursive: true });
171
+ const files = [
172
+ { path: join(target, 'element.mjs'), content: elementTemplate(slug, propNames) },
173
+ { path: join(target, 'manifest.json'), content: manifestTemplate(slug, name, propNames) },
174
+ { path: join(target, 'README.md'), content: readmeTemplate(slug) }
175
+ ];
176
+ for (const file of files) {
177
+ await writeFile(file.path, file.content, 'utf8');
178
+ }
179
+ return {
180
+ directory: target,
181
+ files: files.map(file => basename(file.path)),
182
+ protocolVersion: PROTOCOL_VERSION,
183
+ nextStep: `element.mjs ve manifest.json dosyalarını komponentinize göre düzenleyin, sonra external_component_publish ile bundlePath='${join(target, 'element.mjs')}' ve manifestPath='${join(target, 'manifest.json')}' vererek yayınlayın.`
184
+ };
185
+ }));
186
+ server.registerTool('external_component_publish', {
187
+ title: 'Harici komponenti yayınla',
188
+ description: "Komponent kaydını ve paketini platforma yükler. Paket sunucudaki doğrulama hattından geçer (şema + güvenlik taraması); block bulgusu varsa depolamaya hiç yazılmaz ve rapor döner. Kabul edilen paket taslak (draft) olarak kaydedilir — Studio önizlemesinde görünür, yayınlanmış uygulamada görünmez; yayına almak için external_component_activate. Dosyalar gateway sürecinin okuyabildiği yerel yollardır, içerikleri model bağlamına girmez.",
189
+ inputSchema: {
190
+ project: projectParam,
191
+ environment: environmentParam,
192
+ applicationId: guid,
193
+ bundlePath: z.string().min(1).describe('Tek dosyalık ES module paketinin yerel yolu (.mjs)'),
194
+ manifestPath: z.string().min(1).describe('manifest.json yerel yolu; slug, name, definition ve protocolVersion buradan okunur'),
195
+ version: versionParam
196
+ },
197
+ annotations: writes
198
+ }, handle(config, async ({ project, environment, applicationId, bundlePath, manifestPath, version }) => {
199
+ const manifestFile = resolve(manifestPath);
200
+ const bundleFile = resolve(bundlePath);
201
+ let manifest;
202
+ try {
203
+ manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
204
+ }
205
+ catch (error) {
206
+ throw new PlatformApiError(`external_component_publish: manifest okunamadı ya da geçerli JSON değil — ${manifestFile} (${error instanceof Error ? error.message : String(error)}).`);
207
+ }
208
+ const slug = typeof manifest.slug === 'string' ? manifest.slug : undefined;
209
+ if (!slug || !slugPattern.test(slug)) {
210
+ throw new PlatformApiError(`external_component_publish: manifest'teki slug 'x-' ile başlamalı ve küçük harf/rakam/tire içermeli — okunan: ${String(manifest.slug)}.`);
211
+ }
212
+ let bytes;
213
+ try {
214
+ bytes = await readFile(bundleFile);
215
+ }
216
+ catch (error) {
217
+ throw new PlatformApiError(`external_component_publish: paket okunamadı — ${bundleFile} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`);
218
+ }
219
+ if (bytes.byteLength > maxBundleBytes) {
220
+ throw new PlatformApiError(`external_component_publish: paket ${maxBundleBytes} bayt sınırını aşıyor (${bytes.byteLength} B).`);
221
+ }
222
+ const record = await platform.tenant('POST', project, `/components/${applicationId}/external`, {
223
+ environment,
224
+ body: {
225
+ slug,
226
+ name: manifest.name,
227
+ description: manifest.description,
228
+ icon: manifest.icon,
229
+ categories: manifest.categories ?? [],
230
+ definition: manifest.definition,
231
+ protocolVersion: manifest.protocolVersion ?? PROTOCOL_VERSION
232
+ }
233
+ });
234
+ const bundle = await platform.tenant('POST', project, `/components/${applicationId}/external/${slug}/bundle`, {
235
+ environment,
236
+ multipart: [
237
+ { name: 'Version', value: version },
238
+ { name: 'Bundle', filename: basename(bundleFile), contentType: 'text/javascript', content: bytes }
239
+ ]
240
+ });
241
+ return {
242
+ component: record,
243
+ bundle: bundle?.data,
244
+ ...summarizeFindings(bundle?.data?.findings)
245
+ };
246
+ }));
247
+ server.registerTool('external_component_activate', {
248
+ title: 'Paket sürümünü yayına al',
249
+ description: "Taslak bir paket sürümünü yayına alır; yalnızca metadata değişir, platform yeniden başlatılmaz ve açık oturumlar bozulmaz. Eski bir sürüm verilirse aynı mekanizma geri alma (rollback) olarak çalışır. Doğrulama uyarısı olan bir sürüm acknowledgeWarnings:true olmadan aktive edilemez.",
250
+ inputSchema: {
251
+ project: projectParam,
252
+ environment: environmentParam,
253
+ applicationId: guid,
254
+ slug: slugParam,
255
+ version: versionParam,
256
+ acknowledgeWarnings: z.boolean().optional().describe('Doğrulama uyarılarını okuyup kabul ettiğinizi bildirir; uyarısı olan sürümlerde zorunlu')
257
+ },
258
+ annotations: updates
259
+ }, handle(config, ({ project, environment, applicationId, slug, version, acknowledgeWarnings }) => platform.tenant('POST', project, `/components/${applicationId}/external/${slug}/activate`, {
260
+ environment,
261
+ body: { version, acknowledgeWarnings: acknowledgeWarnings === true }
262
+ })));
263
+ server.registerTool('external_component_versions', {
264
+ title: 'Paket sürümlerini listele',
265
+ description: 'Bir harici komponentin yayınlanmış paket sürümlerini, hangisinin aktif olduğunu ve her sürümün doğrulama raporunu listeler. Geri alınacak sürümü buradan seçin.',
266
+ inputSchema: {
267
+ project: projectParam,
268
+ environment: environmentParam,
269
+ applicationId: guid,
270
+ slug: slugParam
271
+ },
272
+ annotations: readOnly
273
+ }, handle(config, ({ project, environment, applicationId, slug }) => platform.tenant('GET', project, `/components/${applicationId}/external/${slug}/versions`, { environment })));
274
+ server.registerTool('external_component_delete', {
275
+ title: 'Harici komponenti sil',
276
+ description: 'Yayınlanan komponent kaydını siler. Sayfalarda kullanılıyorsa o sayfalar komponenti kaybeder ve yerinde bir hata kartı görünür — silmeden önce dependency_graph ile kullanımını kontrol edin.',
277
+ inputSchema: {
278
+ project: projectParam,
279
+ environment: environmentParam,
280
+ applicationId: guid,
281
+ id: guid.describe('external_component_list çıktısındaki komponent id')
282
+ },
283
+ annotations: destructive
284
+ }, handle(config, ({ project, environment, applicationId, id }) => platform.tenant('DELETE', project, `/components/${applicationId}/${id}`, { environment })));
285
+ }
@@ -1,17 +1,30 @@
1
1
  import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
2
2
  import { normalizeComponentDefaults, componentDefaultsNote, blockSizeNote, heightTypeNote, invalidScreenEventNames, invalidWidgetTypeNodes, verticalGapWarnings, withNormalization } from '../knowledge/normalize.js';
3
- import { fetchHost, layoutUpdateBody, schemaParam, eventsParam } from './screens.js';
3
+ import { fetchHost, layoutUpdateBody, schemaParam, eventsParam, modeParam, summarize, findNode } from './screens.js';
4
4
  import { applyCatalogLayoutRules } from './components.js';
5
5
  import { PlatformApiError } from '../platform/errors.js';
6
6
  import { handle } from '../platform/responses.js';
7
7
  import { z } from 'zod';
8
8
  const viewsParam = schemaParam.describe("Layout'un komponent ağacı. DİKKAT: layout'ta şema alanının adı 'schema' değil 'views'tir ve sayfa micro-op hattı (page_schema_patch) layout'a uygulanmaz. İçinde MUTLAKA bir { type: 'slot' } komponenti olmalı — sayfa içeriği YALNIZ slot'un içine render edilir, slot'suz layout'a bağlanan sayfalar boş görünür.");
9
+ function countSlots(nodes) {
10
+ if (!Array.isArray(nodes))
11
+ return 0;
12
+ return nodes.reduce((total, node) => {
13
+ if (!node || typeof node !== 'object')
14
+ return total;
15
+ const record = node;
16
+ return total + (record.type === 'slot' ? 1 : 0) + countSlots(record.children);
17
+ }, 0);
18
+ }
9
19
  function slotWarning(views) {
10
- const hasSlot = (nodes) => Array.isArray(nodes) && nodes.some(node => Boolean(node) && typeof node === 'object' &&
11
- (node.type === 'slot' || hasSlot(node.children)));
12
- return hasSlot(views)
13
- ? []
14
- : ["LAY-002: layout'ta 'slot' tipli komponent YOK — Studio ve runtime, sayfa şemasını yalnız layout'taki slot'un içine yerleştirir (page.mappers: layout varsa render şeması layout views'üdür); bu layout'a bağlanan her sayfanın içeriği görünmez olur. views'e bir { type: 'slot' } komponenti ekleyin ve page_state.layoutSlots ile doğrulayın."];
20
+ const slots = countSlots(views);
21
+ if (slots === 0) {
22
+ return ["LAY-002: layout'ta 'slot' tipli komponent YOK — Studio ve runtime, sayfa şemasını yalnız layout'taki slot'un içine yerleştirir (page.mappers: layout varsa render şeması layout views'üdür); bu layout'a bağlanan her sayfanın içeriği görünmez olur. views'e bir { type: 'slot' } komponenti ekleyin ve page_state.layoutSlots ile doğrulayın."];
23
+ }
24
+ if (slots > 1) {
25
+ return [`LAY-006: layout'ta ${slots} adet 'slot' var — enjeksiyon HER slot'a AYRI AYRI kopyalar (layout.functions.ts injectPageSchemaIntoSlots ağacı dolaşır ve type==='slot' gören her düğümün children'ına sayfa şemasının klonunu yazar; önizlemede page.mappers.mapPageLayoutToPreview aynısını yapar). Sonuç: sayfa birden çok kez çizilir ve aynı elementId'ler tekrarlanır. Tek slot bırakın.`];
26
+ }
27
+ return [];
15
28
  }
16
29
  export function registerLayoutTools(server, platform) {
17
30
  const config = platform.config;
@@ -27,6 +40,35 @@ export function registerLayoutTools(server, platform) {
27
40
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, id: guid },
28
41
  annotations: readOnly
29
42
  }, handle(config, ({ project, environment, applicationId, id }) => platform.tenant('GET', project, `/layouts/${applicationId}/${id}`, { environment })));
43
+ server.registerTool('layout_schema_get', {
44
+ title: "Layout komponent ağacını oku",
45
+ description: "Layout'un komponent ağacını (views) okur — page_schema_get'in layout karşılığı. DİKKAT: layout'ta micro-op hattı YOKTUR (backend'de PATCH /layouts/.../schema ucu yok), yani buradan okunan updatedAt bir expectedUpdatedAt kontrolünde kullanılamaz; değişiklik layout_update ile tam gövde olarak yazılır.",
46
+ inputSchema: {
47
+ project: projectParam,
48
+ environment: environmentParam,
49
+ applicationId: guid,
50
+ id: guid,
51
+ mode: modeParam,
52
+ componentId: z.string().optional().describe("subtree kipinde kök alınacak komponentin elementId'si")
53
+ },
54
+ annotations: readOnly
55
+ }, handle(config, async ({ project, environment, applicationId, id, mode, componentId }) => {
56
+ const layout = await fetchHost(platform, 'layout', project, environment, applicationId, id);
57
+ const audit = layout.audit;
58
+ const updatedAt = audit?.updatedAt ?? audit?.createdAt;
59
+ const views = (layout.views ?? []);
60
+ if (mode === 'full')
61
+ return { updatedAt, views };
62
+ if (mode === 'subtree') {
63
+ if (!componentId)
64
+ throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
65
+ const subtree = findNode(views, componentId);
66
+ if (!subtree)
67
+ throw new PlatformApiError(`Layout'ta '${componentId}' id'li bir komponent yok; layout_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
68
+ return { updatedAt, views: subtree };
69
+ }
70
+ return { updatedAt, views: views.map(node => summarize(node)) };
71
+ }));
30
72
  server.registerTool('layout_create', {
31
73
  title: 'Layout oluştur',
32
74
  description: "Yeni layout oluşturur. Şablon için mevcut bir layout'u layout_get ile alın; slot kurgusunu page_state çıktısındaki layoutSlots ile doğrulayın.",
@@ -119,7 +119,7 @@ export function registerMicroserviceTools(server, platform) {
119
119
  }));
120
120
  server.registerTool('microservice_create', {
121
121
  title: 'Microservice oluştur',
122
- description: "Yeni microservice tanımlar (definition = CreateMicroserviceCommand). İlk kullanımdan önce platform_guide('microservice') okuyun: kaynak seçimi (table > query) ve selectFields disiplini. Auth/oturum gerektiren uçlarda definition.middlewareIds ile middleware bağlayın; kimlik kapsaması filtrede source:'expression' + value:'user.id' ile yapılır (platform_guide('middleware')). Gövde politika kontrolünden geçer.",
122
+ description: "Yeni microservice tanımlar (definition = CreateMicroserviceCommand). İlk kullanımdan önce platform_guide('microservice') okuyun: kaynak seçimi (table > query) ve selectFields disiplini. 'description' ZORUNLUDUR (MS-014, block): çağıranın bu uçtan ne elde ettiğini yazın. Auth/oturum gerektiren uçlarda definition.middlewareIds ile middleware bağlayın; kimlik kapsaması filtrede source:'expression' + value:'user.id' ile yapılır (platform_guide('middleware')). Ucun gerçek bir tuzağı varsa (auth istemiyor, sayfalama yok, atomik değil) 'caution' alanına yazın — OPSİYONELDİR, tuzak yoksa boş bırakın. Gövde politika kontrolünden geçer.",
123
123
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, definition: definitionParam, policyOverride: policyOverrideParam },
124
124
  annotations: writes
125
125
  }, handle(config, async ({ project, environment, applicationId, definition, policyOverride }) => {
@@ -130,7 +130,7 @@ export function registerMicroserviceTools(server, platform) {
130
130
  }));
131
131
  server.registerTool('microservice_update', {
132
132
  title: 'Microservice güncelle',
133
- description: 'Microservice tanımını günceller (tam gövde). Hedefli değişiklik için microservice_patch tercih edin. Gövde politika kontrolünden geçer.',
133
+ description: 'Microservice tanımını günceller (tam gövde). Hedefli değişiklik için microservice_patch tercih edin. \'description\' ZORUNLUDUR (MS-014, block). Ucun gerçek bir tuzağı varsa (auth istemiyor, sayfalama yok, atomik değil) \'caution\' alanına yazın — OPSİYONELDİR, tuzak yoksa boş bırakın. Gövde politika kontrolünden geçer.',
134
134
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, id: guid, definition: definitionParam, policyOverride: policyOverrideParam },
135
135
  annotations: updates
136
136
  }, handle(config, async ({ project, environment, applicationId, id, definition, policyOverride }) => {
@@ -1,13 +1,15 @@
1
1
  import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
2
2
  import { orphanedFlowWarnings, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, blockSizeNote, heightTypeNote, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, invalidScreenEventNames, invalidWidgetTypeNodes, withNormalization, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
3
- import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap } from './screens.js';
3
+ import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode } from './screens.js';
4
4
  import { applyCatalogLayoutRules, fetchComponentEventNames } from './components.js';
5
5
  import { formBindingWarnings } from '../knowledge/coverage.js';
6
6
  import { PlatformApiError } from '../platform/errors.js';
7
- import { handle } from '../platform/responses.js';
7
+ import { handle, toolResult, toolError } from '../platform/responses.js';
8
+ import { StudioScreenshotter } from '../platform/screenshot.js';
9
+ import { writeFile } from 'node:fs/promises';
10
+ import { resolve } from 'node:path';
8
11
  import { z } from 'zod';
9
12
  const opsParam = z.array(z.record(z.unknown())).describe("Micro-op listesi. Op sözlüğü: replace_prop {target, prop, value} | set_binding {target, prop, expression: '{{ Page.variables.x }}'} | add_component {parent (üst elementId ya da 'root'), index?, component} | remove_component {target} | move_component {target, parent, index?, position: {x, y}} | set_event {target, event, eventValue} | remove_event {target, event} (gateway emülasyonu: platformun micro-op'u olay SİLEMEZ, eventValue: null yalnız değersiz ölü kayıt bırakır — bu op şemayı okuyup olayı diziden çıkarır ve sayfayı tam gövdeyle geri yazar) | set_validations {target, validations: [{type, message, value, isActive}]} (komponentin validation listesini TAMAMEN değiştirir; boş dizi hepsini siler). target = elementId.");
10
- const modeParam = z.enum(['summary', 'subtree', 'full']).optional().describe("summary (varsayılan): yalnız id/elementId/name/type ağacı — hedef bulmak için bunu kullanın. subtree: componentId ile tek alt ağaç. full: tüm şema.");
11
13
  /**
12
14
  * PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
13
15
  * komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
@@ -46,28 +48,6 @@ function invalidComponentNames(nodes) {
46
48
  }
47
49
  return found;
48
50
  }
49
- function summarize(node) {
50
- const summary = {
51
- id: node.id,
52
- elementId: node.elementId,
53
- name: node.name,
54
- type: node.type
55
- };
56
- if (Array.isArray(node.children) && node.children.length > 0) {
57
- summary.children = node.children.map(summarize);
58
- }
59
- return summary;
60
- }
61
- function findNode(nodes, target) {
62
- for (const node of nodes ?? []) {
63
- if (node.elementId === target || String(node.id) === target)
64
- return node;
65
- const found = findNode(node.children, target);
66
- if (found)
67
- return found;
68
- }
69
- return undefined;
70
- }
71
51
  function removeNodeFromTree(nodes, target) {
72
52
  for (let index = 0; index < nodes.length; index++) {
73
53
  if (nodes[index].elementId === target)
@@ -235,10 +215,11 @@ export function registerPageTools(server, platform) {
235
215
  applicationSlug: z.string().min(1).describe("Uygulamanın slug'ı (application_list / project_overview)"),
236
216
  pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list)"),
237
217
  view: z.enum(['summary', 'full']).optional().describe('summary (varsayılan): şema/layout ağaçları id/name/type özetine indirgenir. full: ham gövde — büyük olabilir'),
218
+ resolveStyles: z.boolean().optional().describe("summary görünümünde her düğüme EFEKTİF stilini ekler: tema componentStyles → preset → düğümün kendi stili kaskadı çözülür (aibox'ta tipli stil grupları da derlenir ve TABAN katman olur). styleLayers hangi katmanın konuştuğunu söyler — 'bu kutu neden böyle görünüyor' sorusunun cevabı budur."),
238
219
  anonymous: z.boolean().optional().describe("true: istek HİÇBİR Authorization header'ı olmadan gider — 'korumalı sayfa tokensız ne döner, şema sızıyor mu?' sorusunun cevabı budur. Korumalı sayfada middlewareBlocked bekleyin.")
239
220
  },
240
221
  annotations: readOnly
241
- }, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, anonymous }) => {
222
+ }, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous }) => {
242
223
  let response;
243
224
  try {
244
225
  response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous });
@@ -258,20 +239,107 @@ export function registerPageTools(server, platform) {
258
239
  return response;
259
240
  const body = (response?.data ?? {});
260
241
  const layout = body.layout;
242
+ const theme = body.project?.theme;
243
+ // undefined: stil raporu hiç yazılmaz. null: tema yok ama kaskad yine çözülür.
244
+ const styleSource = resolveStyles ? (theme ?? null) : undefined;
261
245
  const asNodes = (value) => (Array.isArray(value) ? value : []);
262
246
  const named = (value) => (Array.isArray(value) ? value : []).map(entry => ({ id: entry.id, name: entry.name, slug: entry.slug }));
263
247
  return {
264
248
  page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, isHomepage: body.isHomepage },
265
249
  events: body.events ?? [],
266
- layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(summarize) } : null,
267
- schema: asNodes(body.schema).map(summarize),
250
+ layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(node => summarize(node, styleSource)) } : null,
251
+ schema: asNodes(body.schema).map(node => summarize(node, styleSource)),
268
252
  widgets: named(body.widgets),
269
253
  flows: named(body.flows),
270
254
  variableKeys: Object.keys((body.variables ?? {})),
271
255
  hasTheme: Boolean(body.project?.theme),
272
- note: "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f); tam gövde gerekirse view:'full'."
256
+ note: resolveStyles
257
+ ? "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f). resolvedStyle = componentStyles → preset → düğüm stili kaskadı (aibox'ta tipli grupların derlemesi TABAN katmandır); styleLayers hangi katmanların konuştuğunu söyler."
258
+ : "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f); tam gövde gerekirse view:'full'. Bir düğümün ekranda GERÇEKTEN aldığı stili görmek için resolveStyles:true."
273
259
  };
274
260
  }));
261
+ const screenshotter = new StudioScreenshotter(config);
262
+ const maxInlinePngBytes = 5 * 1024 * 1024;
263
+ server.registerTool('page_screenshot', {
264
+ title: 'Sayfanın ekran görüntüsünü al',
265
+ description: "Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve hata/boş durum karesi yine döner — bu da bir doğrulamadır. STUDIO_BASE_URL tanımlı olmalı; ilk kullanım için `npx playwright-core install chromium` gerekir.",
266
+ inputSchema: {
267
+ project: projectParam,
268
+ environment: environmentParam,
269
+ applicationSlug: z.string().min(1).describe("Uygulamanın slug'ı (application_list / project_overview)"),
270
+ pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list)"),
271
+ viewport: z.object({
272
+ width: z.number().int().min(320).max(3840),
273
+ height: z.number().int().min(240).max(2160)
274
+ }).optional().describe('Varsayılan 1280x800 (deviceScaleFactor 1)'),
275
+ fullPage: z.boolean().optional().describe('true: kaydırılan kısım dahil tüm sayfa; varsayılan yalnız viewport karesi'),
276
+ delayMs: z.number().int().min(0).max(10000).optional().describe('Animasyon/geç gelen veri için EK bekleme; 600 ms yerleşme beklemesi her zaman uygulanır'),
277
+ anonymous: z.boolean().optional().describe("true: NG_AccessToken çerezi enjekte edilmez — anonim ziyaretçinin gördüğü kare. Korumalı sayfada middlewareBlocked bekleyin."),
278
+ outPath: z.string().min(1).optional().describe('Verilirse PNG bu yerel yola yazılır ve inline görüntü DÖNMEZ; yalnız özet (yol, boyut) döner — büyük fullPage kareleri için')
279
+ },
280
+ annotations: readOnly
281
+ }, async ({ project, environment, applicationSlug, pageSlug, viewport, fullPage, delayMs, anonymous, outPath }) => {
282
+ try {
283
+ if (!config.studioBaseUrl) {
284
+ throw new PlatformApiError("page_screenshot için STUDIO_BASE_URL tanımlı değil. Studio arayüzünün kök adresini .env'e ekleyin (yerel geliştirmede http://localhost:3000, dağıtımda nginx adresi).");
285
+ }
286
+ const resolvedEnvironment = platform.resolveEnvironment(environment);
287
+ const lane = resolvedEnvironment === 'development' ? 'dev' : resolvedEnvironment === 'production' ? 'view' : undefined;
288
+ if (!lane) {
289
+ throw new PlatformApiError(`page_screenshot yalnız 'development' (/dev) ve 'production' (/view) ortamlarını görüntüleyebilir; '${resolvedEnvironment}' için Studio'da karşılık gelen bir rota yok.`);
290
+ }
291
+ const url = `${config.studioBaseUrl}/${lane}/${platform.resolveProject(project)}/${applicationSlug}/${pageSlug}`;
292
+ const size = viewport ?? { width: 1280, height: 800 };
293
+ const shot = await screenshotter.capture({
294
+ url,
295
+ apiPathMarker: `/${applicationSlug}/p/${pageSlug}`,
296
+ token: anonymous ? undefined : await platform.accessToken(),
297
+ cookieUrl: config.studioBaseUrl,
298
+ viewport: size,
299
+ fullPage: fullPage === true,
300
+ delayMs: delayMs ?? 0
301
+ });
302
+ const meta = {
303
+ url,
304
+ environment: resolvedEnvironment,
305
+ viewport: size,
306
+ fullPage: fullPage === true,
307
+ sizeBytes: shot.png.byteLength,
308
+ elapsedMs: shot.elapsedMs,
309
+ ...(shot.apiStatus === undefined ? {} : { apiStatus: shot.apiStatus }),
310
+ ...(shot.apiStatus !== undefined && shot.apiStatus >= 300
311
+ ? { middlewareBlocked: true, note: 'Sayfanın veri isteği reddedildi — karede hata/boş durum görünüyor olabilir. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır (page_runtime_get ile karşılaştırın).' }
312
+ : {}),
313
+ ...(shot.renderWaitTimedOut
314
+ ? { renderWaitTimedOut: true, note: "Sayfanın veri isteği gözlenmedi — kare hata/boş durumu gösteriyor olabilir; STUDIO_BASE_URL ve slug'ları doğrulayın." }
315
+ : {})
316
+ };
317
+ if (outPath) {
318
+ const absolute = resolve(outPath);
319
+ try {
320
+ await writeFile(absolute, shot.png);
321
+ }
322
+ catch (error) {
323
+ const message = error instanceof Error ? error.message : String(error);
324
+ throw new PlatformApiError(`page_screenshot: dosya yazılamadı — ${absolute} (${message}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`);
325
+ }
326
+ return toolResult({ path: absolute, ...meta }, config);
327
+ }
328
+ if (shot.png.byteLength > maxInlinePngBytes) {
329
+ throw new PlatformApiError(`Görüntü ${shot.png.byteLength} B — inline sınırı (${maxInlinePngBytes} B) aşıyor. outPath verin (dosyaya yazılır, model bağlamına girmez) ya da fullPage'i kapatıp viewport'u küçültün.`);
330
+ }
331
+ return {
332
+ content: [
333
+ { type: 'image', data: shot.png.toString('base64'), mimeType: 'image/png' },
334
+ { type: 'text', text: JSON.stringify(meta) }
335
+ ],
336
+ structuredContent: meta
337
+ };
338
+ }
339
+ catch (error) {
340
+ return toolError(error);
341
+ }
342
+ });
275
343
  server.registerTool('page_schema_get', {
276
344
  title: 'Sayfa şemasını oku',
277
345
  description: "Sayfanın komponent şemasını okur; yanıttaki updatedAt değeri page_schema_patch'in expectedUpdatedAt kontrolünde kullanılır.",
@@ -297,7 +365,7 @@ export function registerPageTools(server, platform) {
297
365
  throw new PlatformApiError(`Sayfada '${componentId}' id'li bir komponent yok; page_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
298
366
  return { updatedAt, schema: subtree };
299
367
  }
300
- return { updatedAt, schema: (page.schema ?? []).map(summarize) };
368
+ return { updatedAt, schema: (page.schema ?? []).map(node => summarize(node)) };
301
369
  }));
302
370
  server.registerTool('page_state', {
303
371
  title: 'Sayfa durumunu getir',
@@ -13,7 +13,7 @@ export function registerPlatformTools(server, platform) {
13
13
  }, handle(config, () => platform.org('GET', '/connect/userinfo')));
14
14
  server.registerTool('platform_guide', {
15
15
  title: 'Platform rehberi oku',
16
- description: "Modülün iş kuralları rehberini döner (workflow, microservice, entity, page, component, html, middleware, view, expressions, translation, getting-started, modify-existing). Bir modülde İLK yazma işleminden önce ilgili rehberi okuyun; sayfa/layout/flow işleri için page, komponent modeli/olay sözleşmeleri için component, hazır komponentler yetmediğinde HTML kaçış kapısı (Mustache, fireEvent, script kuralları) için html, auth/oturum/yetki için middleware, tam metin arama indeksi için view, ifade yazarken kullanılabilir fonksiyon/bağlam sözlüğü (system.*, file.*, Page.*, state.*, Steps…) için expressions, sayfa/içerik çevirileri (çoklu dil) için translation rehberi. Örnek gövdeler için ngapps://examples kaynaklarına bakın.",
16
+ description: "Modülün iş kuralları rehberini döner (workflow, microservice, entity, page, layout, component, html, middleware, view, expressions, translation, getting-started, modify-existing). Bir modülde İLK yazma işleminden önce ilgili rehberi okuyun; sayfa/layout/flow işleri için page, yerleşim kabuğunun kendi kuralları (tek slot, views alanı, micro-op yokluğu, mobil yığılma) için layout, komponent modeli/olay sözleşmeleri için component, hazır komponentler yetmediğinde HTML kaçış kapısı (Mustache, fireEvent, script kuralları) için html, auth/oturum/yetki için middleware, tam metin arama indeksi için view, ifade yazarken kullanılabilir fonksiyon/bağlam sözlüğü (system.*, file.*, Page.*, state.*, Steps…) için expressions, sayfa/içerik çevirileri (çoklu dil) için translation rehberi. Örnek gövdeler için ngapps://examples kaynaklarına bakın.",
17
17
  inputSchema: {
18
18
  guide: z.string().describe('Rehber adı; bilinmeyen ad verilirse mevcut rehberler listelenir')
19
19
  },
@@ -27,7 +27,7 @@ export function registerPlatformTools(server, platform) {
27
27
  }));
28
28
  server.registerTool('platform_gap_report', {
29
29
  title: 'Kabiliyet boşluğu bildir',
30
- description: "Yapmak İSTEDİĞİNİZ ama platform desteklemediği için yapamadığınız her noktada BUNU ÇAĞIRIN — tasarımı sessizce küçültmeyin. Bu çağrı platforma gitmez, yalnızca günlüğe 'KABİLİYET BOŞLUĞU' bloğu yazar; hata günlüğünde görünmeyen bu kayıtlar ürün yol haritasının girdisidir. Önce rehbere bakıp (platform_guide, özellikle hazır komponent yetmiyorsa 'html' kaçış kapısı) gerçekten desteklenmediğinden emin olun: desteklenen bir şeyi 'yok' diye bildirmek yanlış yol haritası üretir.",
30
+ description: "Yapmak İSTEDİĞİNİZ ama platform desteklemediği için yapamadığınız her noktada BUNU ÇAĞIRIN — tasarımı sessizce küçültmeyin. Bu çağrı platforma gitmez, yalnızca günlüğe 'KABİLİYET BOŞLUĞU' bloğu yazar; hata günlüğünde görünmeyen bu kayıtlar ürün yol haritasının girdisidir. Önce rehbere bakıp (platform_guide; hazır komponent yetmiyorsa tek seferlik için 'html' kaçış kapısı, kalıcı bir ihtiyaç içinse 'external-component' — kendi komponentinizi yazıp yayınlayabilirsiniz) gerçekten desteklenmediğinden emin olun: desteklenen bir şeyi 'yok' diye bildirmek yanlış yol haritası üretir.",
31
31
  inputSchema: {
32
32
  goal: z.string().min(1).describe('Ne yapmak istediniz — kullanıcıya görünen sonuç olarak yazın ("randevu listesini hasta adına göre süzmek")'),
33
33
  blocker: z.string().min(1).describe('Neden yapamadınız — hangi kabiliyetin eksik olduğu ("uçta X yok", "komponentte Y prop\'u yok")'),