@appaflytech/zeyrek-ai-mcp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.env.example +46 -0
  2. package/README.md +260 -0
  3. package/dist/config.js +128 -0
  4. package/dist/index.js +88 -0
  5. package/dist/knowledge/componentCatalog.js +438 -0
  6. package/dist/knowledge/componentEvents.js +164 -0
  7. package/dist/knowledge/coverage.js +60 -0
  8. package/dist/knowledge/normalize.js +1309 -0
  9. package/dist/knowledge/outlines.js +167 -0
  10. package/dist/knowledge/policy.js +44 -0
  11. package/dist/knowledge/reservedTables.js +75 -0
  12. package/dist/knowledge/rules.js +1725 -0
  13. package/dist/observability/trace.js +337 -0
  14. package/dist/platform/errors.js +132 -0
  15. package/dist/platform/http.js +169 -0
  16. package/dist/platform/responses.js +94 -0
  17. package/dist/platform/token.js +79 -0
  18. package/dist/resources/examples/entity-pair-m2m.json +105 -0
  19. package/dist/resources/examples/flow-list-to-form.json +65 -0
  20. package/dist/resources/examples/flow-load-table.json +178 -0
  21. package/dist/resources/examples/microservice-table-list.json +49 -0
  22. package/dist/resources/examples/middleware-authenticate.json +194 -0
  23. package/dist/resources/examples/page-detail-profile.json +95 -0
  24. package/dist/resources/examples/page-dynamic-form.json +221 -0
  25. package/dist/resources/examples/page-form-upsert.json +279 -0
  26. package/dist/resources/examples/page-html-embed.json +106 -0
  27. package/dist/resources/examples/page-sign-in.json +194 -0
  28. package/dist/resources/examples/workflow-basic.json +94 -0
  29. package/dist/resources/guides/component.md +108 -0
  30. package/dist/resources/guides/entity.md +348 -0
  31. package/dist/resources/guides/expressions.md +139 -0
  32. package/dist/resources/guides/getting-started.md +109 -0
  33. package/dist/resources/guides/html.md +106 -0
  34. package/dist/resources/guides/microservice.md +313 -0
  35. package/dist/resources/guides/middleware.md +271 -0
  36. package/dist/resources/guides/modify-existing.md +35 -0
  37. package/dist/resources/guides/page.md +1574 -0
  38. package/dist/resources/guides/view.md +89 -0
  39. package/dist/resources/guides/workflow.md +359 -0
  40. package/dist/resources/knowledge.js +44 -0
  41. package/dist/resources/schemas/CreateDatasourceCommand.json +54 -0
  42. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +133 -0
  43. package/dist/resources/schemas/CreateEntityCommand.json +387 -0
  44. package/dist/resources/schemas/CreateMicroserviceCommand.json +528 -0
  45. package/dist/resources/schemas/CreateQueryCommand.json +43 -0
  46. package/dist/resources/schemas/CreateViewCommand.json +158 -0
  47. package/dist/resources/schemas/CreateWorkflowCommand.json +1719 -0
  48. package/dist/resources/schemas/Filter.json +124 -0
  49. package/dist/resources/schemas/GetRowsQuery.json +122 -0
  50. package/dist/resources/schemas/PreviewMicroserviceCommand.json +565 -0
  51. package/dist/resources/schemas/PreviewQueryCommand.json +74 -0
  52. package/dist/resources/schemas/PreviewWorkflowCommand.json +981 -0
  53. package/dist/resources/schemas/StepDto.json +900 -0
  54. package/dist/resources/schemas/UpdateDatasourceCommand.json +58 -0
  55. package/dist/resources/schemas/UpdateEntityCommand.json +679 -0
  56. package/dist/resources/schemas/UpdateMicroserviceCommand.json +512 -0
  57. package/dist/resources/schemas/UpdateQueryCommand.json +86 -0
  58. package/dist/resources/schemas/UpdateViewCommand.json +153 -0
  59. package/dist/resources/schemas/UpdateWorkflowCommand.json +1719 -0
  60. package/dist/resources/schemas.js +40 -0
  61. package/dist/server.js +63 -0
  62. package/dist/tools/access.js +27 -0
  63. package/dist/tools/applications.js +59 -0
  64. package/dist/tools/automations.js +103 -0
  65. package/dist/tools/common.js +18 -0
  66. package/dist/tools/components.js +280 -0
  67. package/dist/tools/datasources.js +82 -0
  68. package/dist/tools/dependencies.js +17 -0
  69. package/dist/tools/deployments.js +17 -0
  70. package/dist/tools/dtos.js +54 -0
  71. package/dist/tools/entities.js +166 -0
  72. package/dist/tools/files.js +138 -0
  73. package/dist/tools/flows.js +226 -0
  74. package/dist/tools/folders.js +51 -0
  75. package/dist/tools/layouts.js +105 -0
  76. package/dist/tools/microservices.js +198 -0
  77. package/dist/tools/organizations.js +65 -0
  78. package/dist/tools/pages.js +597 -0
  79. package/dist/tools/platformInfo.js +44 -0
  80. package/dist/tools/projects.js +152 -0
  81. package/dist/tools/queries.js +48 -0
  82. package/dist/tools/rows.js +115 -0
  83. package/dist/tools/screens.js +97 -0
  84. package/dist/tools/templates.js +65 -0
  85. package/dist/tools/variables.js +94 -0
  86. package/dist/tools/views.js +58 -0
  87. package/dist/tools/workflows.js +196 -0
  88. package/package.json +49 -0
@@ -0,0 +1,337 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ const defaultDirectory = join(process.cwd(), 'logs');
5
+ const activeTrace = new AsyncLocalStorage();
6
+ const activeRequest = new AsyncLocalStorage();
7
+ const inFlight = new Set();
8
+ const secretKeyPattern = /pass|secret|token|authorization|api[-_]?key|credential|connectionstring/i;
9
+ const detailIndent = 26;
10
+ let level = 'off';
11
+ let logFile;
12
+ let logDirectory = defaultDirectory;
13
+ let maxBodyChars = 2000;
14
+ let sessionLines = [];
15
+ let headerPath = '';
16
+ let ensuredDirectory = '';
17
+ let writingDisabled = false;
18
+ let exitHookInstalled = false;
19
+ let sequence = 0;
20
+ function twoDigits(value) {
21
+ return String(value).padStart(2, '0');
22
+ }
23
+ function clockOf(moment) {
24
+ return `${twoDigits(moment.getHours())}:${twoDigits(moment.getMinutes())}:${twoDigits(moment.getSeconds())}.${String(moment.getMilliseconds()).padStart(3, '0')}`;
25
+ }
26
+ function dayOf(moment) {
27
+ return `${moment.getFullYear()}-${twoDigits(moment.getMonth() + 1)}-${twoDigits(moment.getDate())}`;
28
+ }
29
+ function condense(text, limit) {
30
+ const collapsed = text.replace(/\s+/g, ' ').trim();
31
+ return collapsed.length <= limit ? collapsed : `${collapsed.slice(0, limit)}… (+${collapsed.length - limit} karakter kırpıldı)`;
32
+ }
33
+ function clip(text, limit) {
34
+ const collapsed = text.replace(/\s+/g, ' ').trim();
35
+ return collapsed.length <= limit ? collapsed : `${collapsed.slice(0, limit).trimEnd()}…`;
36
+ }
37
+ function redact(value, depth = 0) {
38
+ if (depth > 8)
39
+ return '…';
40
+ if (Array.isArray(value))
41
+ return value.map(entry => redact(entry, depth + 1));
42
+ if (value && typeof value === 'object') {
43
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => {
44
+ if (secretKeyPattern.test(key))
45
+ return [key, '***'];
46
+ if (/^stack(Trace)?$/i.test(key) && typeof entry === 'string') {
47
+ return [key, `${clip(entry.split(/\r?\n/)[0] ?? '', 240)} … (kalan stack kırpıldı)`];
48
+ }
49
+ return [key, redact(entry, depth + 1)];
50
+ }));
51
+ }
52
+ return value;
53
+ }
54
+ export function renderJson(value) {
55
+ if (value === undefined)
56
+ return '(yok)';
57
+ try {
58
+ return condense(JSON.stringify(redact(value)) ?? String(value), maxBodyChars);
59
+ }
60
+ catch {
61
+ return condense(String(value), maxBodyChars);
62
+ }
63
+ }
64
+ function resolveLogPath() {
65
+ return logFile ?? join(logDirectory, `gateway-${dayOf(new Date())}.log`);
66
+ }
67
+ function write(text) {
68
+ if (level === 'off' || writingDisabled)
69
+ return;
70
+ const path = resolveLogPath();
71
+ try {
72
+ if (ensuredDirectory !== dirname(path)) {
73
+ mkdirSync(dirname(path), { recursive: true });
74
+ ensuredDirectory = dirname(path);
75
+ }
76
+ if (headerPath !== path) {
77
+ const intro = existsSync(path) ? [] : fileIntroLines();
78
+ headerPath = path;
79
+ appendFileSync(path, [...intro, ...sessionLines].join('\n'), 'utf8');
80
+ }
81
+ appendFileSync(path, text, 'utf8');
82
+ }
83
+ catch (error) {
84
+ writingDisabled = true;
85
+ process.stderr.write(`[mcp-gateway] tool günlüğü yazılamadı, günlükleme kapatıldı: ${String(error)}\n`);
86
+ }
87
+ }
88
+ function fileIntroLines() {
89
+ return [
90
+ '═'.repeat(112),
91
+ 'ngapps mcp gateway — tool çağrısı günlüğü. Her tool çağrısı tek bir bloktur:',
92
+ ' amaç tool ne işe yarar (tool tanımındaki açıklama)',
93
+ ' girdi çağrıya gelen parametreler; parola/token/connection string alanları *** ile maskelenir',
94
+ ' adımlar çağrının içeriden geçtiği duraklar, çağrı başlangıcından itibaren geçen süreyle:',
95
+ ' politika = gövde lint kuralları (block bulgusu isteği hiç göndermez)',
96
+ ' düzeltme = gateway otomatik düzeltmeleri (yanıtta gatewayNormalized)',
97
+ ' kimlik →← = /connect/token turu, istek →/yanıt ← = platform API turu',
98
+ ' kırpma ✂ = yanıt boyut sınırı, hata ✕ = yakalanan hata (mesaj + stack)',
99
+ ' SONUÇ çağrının bittiği yer: başarılı/HATA, toplam süre, adım sayısı ve BAŞARILI',
100
+ ' çağrılarda etki özeti (id/slug/applied/kaç kayıt/gatewayNormalized)',
101
+ '',
102
+ 'Ayrıca "KABİLİYET BOŞLUĞU" blokları vardır: ajanın platform desteklemediği için',
103
+ 'yapamadığı ve tasarımı küçülttüğü noktalar (platform_gap_report ile yazılır).',
104
+ 'Bunlar hata DEĞİLDİR — hata günlüğünde hiç görünmezler, ürün kararı için toplanırlar.',
105
+ '',
106
+ 'Arama ipuçları: "SONUÇ : HATA" tüm başarısız çağrılar · "hata ✕" hata satırları ·',
107
+ '"MCP KATMANI REDDETTİ" tool gövdesine hiç girmeyen çağrılar · "#<n>" tek bir çağrı ·',
108
+ '"KABİLİYET BOŞLUĞU" platformun yetmediği noktalar.',
109
+ '═'.repeat(112),
110
+ ''
111
+ ];
112
+ }
113
+ function buildSessionLines(config, transport) {
114
+ const now = new Date();
115
+ return [
116
+ '',
117
+ `── oturum ${dayOf(now)} ${clockOf(now)} · pid ${process.pid} · taşıma ${transport} · org ${config.organization} · proje ${config.defaultProject ?? '(yok)'} · env ${config.defaultEnvironment} · politika ${config.policyMode} · ${config.modules.length} modül · günlük ${level}`,
118
+ ''
119
+ ];
120
+ }
121
+ export function configureLogging(config, transport) {
122
+ level = config.logLevel;
123
+ logFile = config.logFile;
124
+ logDirectory = config.logDirectory ?? defaultDirectory;
125
+ maxBodyChars = config.logMaxBodyChars;
126
+ sessionLines = buildSessionLines(config, transport ?? 'mcp');
127
+ if (transport)
128
+ headerPath = '';
129
+ if (level !== 'off' && !exitHookInstalled) {
130
+ exitHookInstalled = true;
131
+ process.on('exit', () => {
132
+ for (const trace of inFlight) {
133
+ write(renderTrace(trace, { ok: false, summary: 'YARIDA KESİLDİ — süreç tool çağrısı sürerken kapandı' }));
134
+ }
135
+ });
136
+ }
137
+ }
138
+ export function logsDetail() {
139
+ return level === 'detail';
140
+ }
141
+ export function logStep(label, summary, detail = []) {
142
+ const trace = activeTrace.getStore();
143
+ if (!trace || trace.steps.length >= 500)
144
+ return;
145
+ trace.steps.push({
146
+ elapsedMs: Date.now() - trace.startedAt,
147
+ label,
148
+ summary,
149
+ detail: detail.filter(line => line.length > 0).map(line => condense(line, maxBodyChars))
150
+ });
151
+ }
152
+ export function logError(error) {
153
+ if (!activeTrace.getStore())
154
+ return;
155
+ const failure = error instanceof Error ? error : new Error(String(error));
156
+ const status = failure.status;
157
+ const code = failure.code;
158
+ const marks = [status ? `HTTP ${status}` : '', code ? `kod ${code}` : ''].filter(mark => mark.length > 0);
159
+ const stack = (failure.stack ?? '').split('\n').slice(1, 4).map(line => line.trim());
160
+ logStep('hata ✕', `${failure.name}${marks.length > 0 ? ` (${marks.join(', ')})` : ''} — tool isError yanıtına çevirdi`, [
161
+ failure.message,
162
+ ...(level === 'detail' ? stack : [])
163
+ ]);
164
+ }
165
+ function renderTrace(trace, outcome) {
166
+ const lines = [
167
+ `┌─ #${trace.sequence} · ${trace.clock} · ${trace.tool}${trace.title ? ` — ${trace.title}` : ''}`,
168
+ `│ amaç : ${trace.purpose}`,
169
+ `│ girdi : ${trace.input}`,
170
+ trace.steps.length > 0 ? '│ adımlar:' : '│ adımlar: (yok — çağrı platforma çıkmadan gateway içinde bitti)'
171
+ ];
172
+ for (const step of trace.steps) {
173
+ lines.push(`│ ${`+${step.elapsedMs}ms`.padEnd(9)} ${step.label.padEnd(11)} ${step.summary}`);
174
+ for (const entry of step.detail) {
175
+ for (const line of entry.split('\n'))
176
+ lines.push(`│${' '.repeat(detailIndent)}${line}`);
177
+ }
178
+ }
179
+ lines.push(`└─ SONUÇ : ${outcome.ok ? 'başarılı' : 'HATA'} · ${Date.now() - trace.startedAt} ms · ${trace.steps.length} adım · ${outcome.summary}`);
180
+ lines.push('');
181
+ return `${lines.join('\n')}\n`;
182
+ }
183
+ function describeResult(result) {
184
+ const call = result;
185
+ const text = Array.isArray(call?.content)
186
+ ? call.content.filter(entry => entry.type === 'text').map(entry => String(entry.text)).join(' ')
187
+ : '';
188
+ if (call?.isError)
189
+ return { ok: false, summary: clip(text, 400) };
190
+ const effect = summarizeEffect(text);
191
+ return { ok: true, summary: `${Buffer.byteLength(text, 'utf8')} B yanıt${effect ? ` · ${effect}` : ''}` };
192
+ }
193
+ /**
194
+ * Başarılı çağrının NE YAPTIĞINI tek satırda özetler. Eskiden yalnız yanıt boyutu yazılıyordu;
195
+ * günlükten "hangi kayıt oluştu, kaç op uygulandı, gateway neyi düzeltti" okunamıyordu.
196
+ */
197
+ const effectKeys = ['id', 'slug', 'name', 'applied', 'newVersion', 'count', 'total', 'httpStatus'];
198
+ function summarizeEffect(text) {
199
+ if (text.length === 0)
200
+ return '';
201
+ let parsed;
202
+ try {
203
+ parsed = JSON.parse(text);
204
+ }
205
+ catch {
206
+ return '';
207
+ }
208
+ if (Array.isArray(parsed))
209
+ return `dizi=${parsed.length}`;
210
+ if (!parsed || typeof parsed !== 'object')
211
+ return '';
212
+ const record = parsed;
213
+ const marks = [];
214
+ for (const key of effectKeys) {
215
+ const value = record[key];
216
+ if (value === null || value === undefined || typeof value === 'object')
217
+ continue;
218
+ marks.push(`${key}=${clip(String(value), 80)}`);
219
+ }
220
+ for (const key of ['items', 'data', 'rows', 'results']) {
221
+ if (Array.isArray(record[key]))
222
+ marks.push(`${key}=${record[key].length}`);
223
+ }
224
+ if (record.gatewayNormalized)
225
+ marks.push('gatewayNormalized');
226
+ if (Array.isArray(record.findings) && record.findings.length > 0)
227
+ marks.push(`politikaBulgusu=${record.findings.length}`);
228
+ return marks.join(' · ');
229
+ }
230
+ async function traceToolCall(tool, spec, callArgs, callback) {
231
+ const now = new Date();
232
+ const first = callArgs[0];
233
+ const trace = {
234
+ sequence: (sequence += 1),
235
+ tool,
236
+ title: spec.title ?? '',
237
+ purpose: spec.description ? clip(spec.description, 260) : '(açıklama yok)',
238
+ input: first && typeof first === 'object' && Object.keys(first).length > 0 ? renderJson(first) : '(parametresiz)',
239
+ startedAt: now.getTime(),
240
+ clock: clockOf(now),
241
+ steps: []
242
+ };
243
+ const marker = activeRequest.getStore();
244
+ if (marker)
245
+ marker.traced = true;
246
+ inFlight.add(trace);
247
+ try {
248
+ const result = await activeTrace.run(trace, async () => callback(...callArgs));
249
+ write(renderTrace(trace, describeResult(result)));
250
+ return result;
251
+ }
252
+ catch (error) {
253
+ const failure = error instanceof Error ? error : new Error(String(error));
254
+ write(renderTrace(trace, { ok: false, summary: `tool istisna fırlattı — ${failure.name}: ${clip(failure.message, 300)}` }));
255
+ throw error;
256
+ }
257
+ finally {
258
+ inFlight.delete(trace);
259
+ }
260
+ }
261
+ function logRejectedRequest(request, reason) {
262
+ const call = request;
263
+ const target = call?.params?.name ?? call?.params?.uri ?? '';
264
+ const description = reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason);
265
+ write(`${[
266
+ `┌─ ${clockOf(new Date())} · ${call?.method ?? 'mcp isteği'}${target ? ` · ${target}` : ''} · MCP KATMANI REDDETTİ`,
267
+ `│ girdi : ${renderJson(call?.params?.arguments)}`,
268
+ `│ hata : ${clip(description, 600)}`,
269
+ '└─ SONUÇ : HATA · tool gövdesi hiç çalışmadı; istek tool adı/parametre şeması doğrulamasında durdu',
270
+ ''
271
+ ].join('\n')}\n`);
272
+ }
273
+ async function traceResourceRead(name, readArgs, read) {
274
+ const startedAt = Date.now();
275
+ const uri = readArgs[0] instanceof URL ? readArgs[0].href : name;
276
+ const marker = activeRequest.getStore();
277
+ if (marker)
278
+ marker.traced = true;
279
+ try {
280
+ const result = await read(...readArgs);
281
+ write(`[${clockOf(new Date())}] kaynak okundu · ${uri} · ${Buffer.byteLength(JSON.stringify(result) ?? '', 'utf8')} B · ${Date.now() - startedAt} ms\n`);
282
+ return result;
283
+ }
284
+ catch (error) {
285
+ write(`[${clockOf(new Date())}] kaynak HATASI · ${uri} · ${String(error)}\n`);
286
+ throw error;
287
+ }
288
+ }
289
+ export function instrumentServer(server, config) {
290
+ configureLogging(config);
291
+ if (level === 'off')
292
+ return server;
293
+ const registerTool = server.registerTool.bind(server);
294
+ const registerResource = server.registerResource.bind(server);
295
+ server.registerTool = ((tool, spec, callback) => registerTool(tool, spec, (...callArgs) => traceToolCall(tool, spec, callArgs, callback)));
296
+ server.registerResource = ((name, uri, spec, read) => registerResource(name, uri, spec, (...readArgs) => traceResourceRead(name, readArgs, read)));
297
+ const lowLevelServer = server.server;
298
+ const setRequestHandler = lowLevelServer.setRequestHandler.bind(lowLevelServer);
299
+ lowLevelServer.setRequestHandler = ((schema, handler) => setRequestHandler(schema, async (...handlerArgs) => {
300
+ const marker = { traced: false };
301
+ try {
302
+ const result = await activeRequest.run(marker, async () => handler(...handlerArgs));
303
+ if (!marker.traced && result?.isError) {
304
+ logRejectedRequest(handlerArgs[0], result.content?.[0]?.text ?? 'bilinmeyen doğrulama hatası');
305
+ }
306
+ return result;
307
+ }
308
+ catch (error) {
309
+ if (!marker.traced)
310
+ logRejectedRequest(handlerArgs[0], error);
311
+ throw error;
312
+ }
313
+ }));
314
+ return server;
315
+ }
316
+ export function logGatewayEvent(message) {
317
+ write(`[${clockOf(new Date())}] ${message}\n`);
318
+ }
319
+ /**
320
+ * Ajanın "bunu yapamıyorum çünkü platform desteklemiyor" dediği anı kalıcı kılar. Bu bilgi
321
+ * hata günlüğünden çıkmaz: çağrı hiç yapılmadığı için ortada hata yoktur, yalnızca küçültülmüş
322
+ * bir tasarım vardır. Turdan sonra ürün kararı bu kayıtlardan çıkar.
323
+ */
324
+ export function logCapabilityGap(gap) {
325
+ const lines = [
326
+ `┌─ ${clockOf(new Date())} · KABİLİYET BOŞLUĞU · ${gap.severity}${gap.module ? ` · ${gap.module}` : ''}`,
327
+ `│ hedef : ${condense(gap.goal, maxBodyChars)}`,
328
+ `│ engel : ${condense(gap.blocker, maxBodyChars)}`
329
+ ];
330
+ if (gap.attempted && gap.attempted.length > 0) {
331
+ lines.push(`│ denenen: ${gap.attempted.map(entry => condense(entry, 300)).join(' | ')}`);
332
+ }
333
+ lines.push(`│ yerine : ${gap.workaround ? condense(gap.workaround, maxBodyChars) : '(tasarım küçültüldü, alternatif yok)'}`);
334
+ lines.push('└─ SONUÇ : kaydedildi · bu bir hata değil, eksik kabiliyet raporudur');
335
+ lines.push('');
336
+ write(`${lines.join('\n')}\n`);
337
+ }
@@ -0,0 +1,132 @@
1
+ export class PlatformApiError extends Error {
2
+ status;
3
+ code;
4
+ constructor(message, status, code) {
5
+ super(message);
6
+ this.status = status;
7
+ this.code = code;
8
+ this.name = 'PlatformApiError';
9
+ }
10
+ }
11
+ const MaxSurfacedValidationMessages = 20;
12
+ const MaxSurfacedSuggestions = 12;
13
+ function formatPageSchemaError(entry) {
14
+ const where = [entry.componentId, entry.path].filter(part => typeof part === 'string' && part.length > 0).join(' · ');
15
+ const rule = typeof entry.rule === 'string' && entry.rule.length > 0 ? `[${entry.rule}] ` : '';
16
+ const suggestions = Array.isArray(entry.suggestions) && entry.suggestions.length > 0
17
+ ? ` → kabul edilenler: ${entry.suggestions.slice(0, MaxSurfacedSuggestions).join(', ')}${entry.suggestions.length > MaxSurfacedSuggestions ? ` (+${entry.suggestions.length - MaxSurfacedSuggestions})` : ''}`
18
+ : '';
19
+ return `${rule}${where || '?'}: ${entry.message ?? ''}${suggestions}`;
20
+ }
21
+ function collectValidationMessages(payload) {
22
+ const messages = [];
23
+ if (Array.isArray(payload.errors)) {
24
+ for (const entry of payload.errors) {
25
+ if (!entry || typeof entry !== 'object')
26
+ continue;
27
+ const record = entry;
28
+ const isPageSchemaShape = 'rule' in record || 'componentId' in record || 'suggestions' in record;
29
+ messages.push(isPageSchemaShape
30
+ ? formatPageSchemaError(record)
31
+ : `${record.key ?? '?'}: ${record.value ?? ''}`);
32
+ }
33
+ }
34
+ else if (payload.errors && typeof payload.errors === 'object') {
35
+ for (const [field, fieldMessages] of Object.entries(payload.errors)) {
36
+ const flattened = Array.isArray(fieldMessages) ? fieldMessages.join(' | ') : String(fieldMessages);
37
+ messages.push(`${field}: ${flattened}`);
38
+ }
39
+ }
40
+ if (messages.length <= MaxSurfacedValidationMessages)
41
+ return messages;
42
+ return [...messages.slice(0, MaxSurfacedValidationMessages), `… ve ${messages.length - MaxSurfacedValidationMessages} hata daha`];
43
+ }
44
+ /**
45
+ * Sayfa şeması patch'inin üç reddi de "hiçbir op uygulanmadı" anlamına gelir (uygulayıcı önce tüm
46
+ * op'ları bellekte işler, tek hata bile tüm komutu düşürür) — ajanın kısmi uygulama varsayıp
47
+ * planın kalanını göndermesi bu yüzden yanlış olur.
48
+ */
49
+ function pageSchemaHint(code) {
50
+ if (code === 'PageSchemaVersionConflictException') {
51
+ return ' Sayfa siz okuduktan sonra değişti ve HİÇBİR op uygulanmadı. page_schema_get ile yeniden okuyup planı tazeleyin, yeni `version` değerini `baseVersion` olarak gönderin.';
52
+ }
53
+ if (code === 'PageSchemaLockedException') {
54
+ return ' Sayfa şeması başka bir aktörün kilidinde (kilit sahibi mesajda; kilit 60 sn sonra kendiliğinden düşer). Bekleyip tekrar deneyin — aynı anda iki yazar olduğunda kaybeden taraf sessizce ezilmesin diye kilit var.';
55
+ }
56
+ if (code === 'PageSchemaValidationException') {
57
+ return ' Op listesi doğrulama duvarına takıldı ve HİÇBİR op uygulanmadı (hepsi ya da hiçbiri). Alan hatalarını düzeltip tek çağrıda tekrar gönderin; planı önce page_schema_validate ile kuru koşturabilirsiniz.';
58
+ }
59
+ return '';
60
+ }
61
+ /**
62
+ * Bilinen platform bug'larını ajanın tool yüzeyinden teşhis edebilmesi için desen eşlemesi
63
+ * (klinik turu dersi: teşhis DB'ye/koda inmeden yapılabilmeli — görev G-09).
64
+ */
65
+ function diagnoseKnownFailure(status, message, code, context) {
66
+ if (status !== 500)
67
+ return '';
68
+ if (context.path.includes('/row') && message.includes('Sequence contains no matching element')) {
69
+ return " Muhtemel sebep: entity bir application'a bağlı değil — entity_patch ile applicationIds ekleyin (PLT-004).";
70
+ }
71
+ if (message.includes('42P01') || /relation "[^"]+" does not exist/.test(message)) {
72
+ return " Bilinen sınır (K-005/P-01): m2m, çoklu dosya ya da çoklu enum alanı entity_create gövdesindeyken platform junction KAYDINI açar ama fiziksel tabloyu kurmaz. Alanı entity_patch ile yeniden ekleyin; gateway'in entity_create'i bunu artık kendisi iki aşamaya bölüyor, elle kurulmuş eski kayıtlar etkilenir.";
73
+ }
74
+ if (message.includes('cannot be tracked') || message.includes('already being tracked')) {
75
+ return " Bilinen sınır (K-006/P-02): operation:'get' tipli microservice'te PUT, izlenen Application grafiği yüzünden 500 veriyor. Geçici yol: microservice_delete + microservice_create ile yeniden kurun (gövdeyi önce microservice_get ile alın).";
76
+ }
77
+ if (code === 'RowValidationFailed' || message.includes('RowValidationFailed')) {
78
+ return ' Gövdedeki alanlar doğru görünüyorsa muhtemel sebep istemcinin body alanını düşürmesidir (K-002): microservice_execute/debug çağrısında body bir NESNE ya da nesne DİZİSİ olmalı; boş gövdeyle gönderildiyse istemci şemayı desteklemiyor olabilir.';
79
+ }
80
+ return '';
81
+ }
82
+ export function mapPlatformError(status, payload, context) {
83
+ const body = payload && typeof payload === 'object' ? payload : {};
84
+ const code = typeof body.code === 'string' ? body.code : undefined;
85
+ const apiMessage = typeof body.message === 'string' && body.message.length > 0
86
+ ? body.message
87
+ : typeof body.title === 'string' ? body.title : undefined;
88
+ const location = `${context.method} ${context.path}`;
89
+ if (status === 400) {
90
+ const validationMessages = collectValidationMessages(body);
91
+ if (validationMessages.length > 0) {
92
+ return new PlatformApiError(`Doğrulama hatası (${location}): ${validationMessages.join(' ; ')}. Gövde şeması için ilgili ngapps://schemas kaynağına ya da mevcut bir kaydın *_get çıktısına bakın.`, status, code);
93
+ }
94
+ return new PlatformApiError(`Geçersiz istek (${location}): ${apiMessage ?? 'API gövdeyi reddetti.'}`, status, code);
95
+ }
96
+ if (status === 401) {
97
+ const hint = context.authMode === 'local'
98
+ ? 'Gateway konfigürasyonundaki USERNAME/PASSWORD doğru mu kontrol edin.'
99
+ : 'İstemciden geçirilen Authorization header süresi dolmuş olabilir; yeni token alıp MCP kaydını güncelleyin.';
100
+ return new PlatformApiError(`Kimlik doğrulanamadı (${location}). ${hint}`, status, code);
101
+ }
102
+ if (status === 403) {
103
+ return new PlatformApiError(`Yetki yok (${location}). Token'ın sahibi bu işlem için yetkili değil.`, status, code);
104
+ }
105
+ if (status === 404) {
106
+ const environmentHint = context.environment
107
+ ? ` Environment '${context.environment}' doğru mu kontrol edin;`
108
+ : '';
109
+ if (context.method !== 'GET' && /\/flows(\/|$)/.test(context.path)) {
110
+ return new PlatformApiError(`Adımda referans verilen kayıt çözümlenemedi (${location}). Bu uçta 404 akışın kendisini DEĞİL bir adım argümanını kastediyor: flow-trigger adımının args'ında ne flowId ne triggerElementId var (ya da triggerElementId sayfadaki hiçbir elementId ile eşleşmiyor), setter adımının iteratees[].element'i bulunamıyor, ya da bir adım silinmiş bir komponente bakıyor. Adım argümanlarını page_schema_get'teki elementId'lerle karşılaştırın; flow-trigger'ın iki geçerli şekli için platform_guide('page') Kural 1c.`, status, code);
111
+ }
112
+ return new PlatformApiError(`Kayıt bulunamadı (${location}).${environmentHint} id/slug değerini ilgili *_list tool'uyla doğrulayın.`, status, code);
113
+ }
114
+ if (status === 409 || status === 422) {
115
+ const projectConflictHint = status === 409 && context.method === 'POST' && /\/projects\/?$/.test(context.path)
116
+ ? " Mesaj hangi alanın çakıştığını söylemez: proje slug'ı VEYA application slug'ı dolu olabilir. Application slug'ı ORGANİZASYON GENELİNDE tekildir ('web'/'app'/'portal' gibi jenerik adlar büyük olasılıkla doludur) — project_list ile mevcut application slug'larını tarayın."
117
+ : '';
118
+ const validationMessages = collectValidationMessages(body);
119
+ const detail = validationMessages.length > 0 ? ` → ${validationMessages.join(' ; ')}` : '';
120
+ return new PlatformApiError(`İş kuralı reddetti (${location}): ${apiMessage ?? 'çakışma'}${code ? ` [${code}]` : ''}${detail}${projectConflictHint}${pageSchemaHint(code)}`, status, code);
121
+ }
122
+ const fallbackValidationMessages = collectValidationMessages(body);
123
+ const fallbackDetail = fallbackValidationMessages.length > 0 ? ` → ${fallbackValidationMessages.join(' ; ')}` : '';
124
+ return new PlatformApiError(`Platform hatası ${status} (${location}): ${apiMessage ?? 'beklenmeyen hata'}${code ? ` [${code}]` : ''}${fallbackDetail}${diagnoseKnownFailure(status, apiMessage ?? '', code, context)}`, status, code);
125
+ }
126
+ export function mapNetworkError(error, context) {
127
+ const cause = error instanceof Error ? error : new Error(String(error));
128
+ if (cause.name === 'TimeoutError' || cause.name === 'AbortError') {
129
+ return new PlatformApiError(`${context.api} API zaman aşımına uğradı (${context.method} ${context.path}). Uzun süren bir işlemse durumu ilgili *_history/*_status tool'undan izleyin.`);
130
+ }
131
+ return new PlatformApiError(`${context.api} API'ye ulaşılamadı (${context.baseUrl}): ${cause.message}. API ayakta mı? Lokal ortam için docker compose ya da dotnet run kontrol edin.`);
132
+ }
@@ -0,0 +1,169 @@
1
+ import { PlatformApiError, mapNetworkError, mapPlatformError } from './errors.js';
2
+ import { logStep, logsDetail, renderJson } from '../observability/trace.js';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { TokenProvider } from './token.js';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { request } from 'undici';
7
+ export const requestAuthStorage = new AsyncLocalStorage();
8
+ function buildMultipart(parts, boundary) {
9
+ const chunks = [];
10
+ for (const part of parts) {
11
+ const disposition = 'filename' in part
12
+ ? `form-data; name="${part.name}"; filename="${part.filename.replace(/"/g, '')}"\r\nContent-Type: ${part.contentType}`
13
+ : `form-data; name="${part.name}"`;
14
+ chunks.push(Buffer.from(`--${boundary}\r\nContent-Disposition: ${disposition}\r\n\r\n`, 'utf8'));
15
+ chunks.push('filename' in part ? part.content : Buffer.from(part.value, 'utf8'));
16
+ chunks.push(Buffer.from('\r\n', 'utf8'));
17
+ }
18
+ chunks.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));
19
+ return Buffer.concat(chunks);
20
+ }
21
+ function describeMultipart(parts) {
22
+ return Object.fromEntries(parts.map(part => [
23
+ part.name,
24
+ 'filename' in part ? `<dosya ${part.filename} · ${part.contentType} · ${part.content.byteLength} B>` : part.value
25
+ ]));
26
+ }
27
+ function appendQuery(url, query) {
28
+ if (!query)
29
+ return;
30
+ for (const [key, value] of Object.entries(query)) {
31
+ if (value === undefined || value === null || value === '')
32
+ continue;
33
+ if (Array.isArray(value)) {
34
+ for (const entry of value)
35
+ url.searchParams.append(key, String(entry));
36
+ }
37
+ else {
38
+ url.searchParams.append(key, String(value));
39
+ }
40
+ }
41
+ }
42
+ export class Platform {
43
+ config;
44
+ tokens;
45
+ constructor(config, tokens) {
46
+ this.config = config;
47
+ this.tokens = tokens ?? new TokenProvider(config);
48
+ }
49
+ resolveProject(project) {
50
+ const resolved = project ?? this.config.defaultProject;
51
+ if (!resolved) {
52
+ throw new PlatformApiError("Proje belirtilmedi: tool çağrısına 'project' parametresi ekleyin ya da DEFAULT_PROJECT tanımlayın.");
53
+ }
54
+ return resolved;
55
+ }
56
+ resolveEnvironment(environment) {
57
+ return environment ?? this.config.defaultEnvironment;
58
+ }
59
+ async org(method, path, options = {}) {
60
+ const outcome = await this.send('org', method, path, {
61
+ ...options,
62
+ environment: this.resolveEnvironment(options.environment)
63
+ });
64
+ return outcome.body;
65
+ }
66
+ async tenant(method, project, path, options = {}) {
67
+ const outcome = await this.send('tenant', method, `/${this.resolveProject(project)}${path}`, {
68
+ ...options,
69
+ environment: this.resolveEnvironment(options.environment)
70
+ });
71
+ return outcome.body;
72
+ }
73
+ async tenantRaw(method, project, path, options = {}) {
74
+ return this.send('tenant', method, `/${this.resolveProject(project)}${path}`, {
75
+ ...options,
76
+ environment: this.resolveEnvironment(options.environment)
77
+ }, true);
78
+ }
79
+ async authorizationHeader() {
80
+ if (this.config.authMode === 'passthrough') {
81
+ const incoming = requestAuthStorage.getStore();
82
+ if (!incoming) {
83
+ throw new PlatformApiError("Passthrough modda Authorization header'ı gelmedi. MCP kaydına --header \"Authorization: Bearer <token>\" ekleyin.");
84
+ }
85
+ return incoming;
86
+ }
87
+ return `Bearer ${await this.tokens.getToken()}`;
88
+ }
89
+ async send(api, method, path, options, raw = false, isRetry = false) {
90
+ const baseUrl = api === 'org' ? this.config.orgApiBaseUrl : this.config.tenantApiBaseUrl;
91
+ const context = { api, method, path, baseUrl, environment: options.environment, authMode: this.config.authMode };
92
+ const url = new URL(`${baseUrl}${path}`);
93
+ appendQuery(url, options.query);
94
+ const callerHeaders = Object.fromEntries(Object.entries(options.headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]));
95
+ const headers = {
96
+ host: `${this.config.organization}.${this.config.hostSuffix}`,
97
+ accept: 'application/json',
98
+ ...(callerHeaders.authorization || options.anonymous ? {} : { authorization: await this.authorizationHeader() }),
99
+ ...(options.environment ? { 'x-environment': options.environment } : {}),
100
+ ...callerHeaders
101
+ };
102
+ let body;
103
+ if (options.multipart) {
104
+ const boundary = `----ngapps${randomUUID().replace(/-/g, '')}`;
105
+ headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
106
+ body = buildMultipart(options.multipart, boundary);
107
+ }
108
+ else if (options.form) {
109
+ headers['content-type'] = 'application/x-www-form-urlencoded';
110
+ body = options.form.toString();
111
+ }
112
+ else if (options.body !== undefined) {
113
+ headers['content-type'] = 'application/json';
114
+ body = JSON.stringify(options.body);
115
+ }
116
+ const startedAt = Date.now();
117
+ 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)
118
+ ? [`gövde: ${renderJson(options.multipart
119
+ ? describeMultipart(options.multipart)
120
+ : options.form
121
+ ? Object.fromEntries(options.form)
122
+ : options.body)}`]
123
+ : []);
124
+ let statusCode;
125
+ let payload;
126
+ let responseBytes = 0;
127
+ try {
128
+ const response = await request(url, {
129
+ method: method,
130
+ headers,
131
+ body,
132
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs)
133
+ });
134
+ statusCode = response.statusCode;
135
+ if (options.expectBinary && statusCode < 400) {
136
+ const bytes = Buffer.from(await response.body.arrayBuffer());
137
+ responseBytes = bytes.byteLength;
138
+ payload = { contentType: String(response.headers['content-type'] ?? 'application/octet-stream'), bytes };
139
+ }
140
+ else {
141
+ const text = await response.body.text();
142
+ responseBytes = Buffer.byteLength(text, 'utf8');
143
+ try {
144
+ payload = text ? JSON.parse(text) : undefined;
145
+ }
146
+ catch {
147
+ payload = text;
148
+ }
149
+ }
150
+ }
151
+ catch (error) {
152
+ logStep('ağ ✕', `${method} ${url.href} · ${Date.now() - startedAt} ms sonra ulaşılamadı`, [error instanceof Error ? `${error.name}: ${error.message}` : String(error)]);
153
+ throw mapNetworkError(error, context);
154
+ }
155
+ logStep('yanıt ←', `HTTP ${statusCode} · ${Date.now() - startedAt} ms · ${responseBytes} B`, statusCode >= 400 || logsDetail()
156
+ ? [`gövde: ${options.expectBinary && statusCode < 400 ? `<ikili içerik · ${responseBytes} B>` : renderJson(payload)}`]
157
+ : []);
158
+ if (statusCode === 401 && this.config.authMode === 'local' && !isRetry && !options.anonymous) {
159
+ this.tokens.invalidate();
160
+ logStep('token ⟳', 'token reddedildi — önbellek boşaltıldı, istek yeni token ile bir kez daha denenecek');
161
+ return this.send(api, method, path, options, raw, true);
162
+ }
163
+ if (raw)
164
+ return { httpStatus: statusCode, body: payload ?? null };
165
+ if (statusCode >= 400)
166
+ throw mapPlatformError(statusCode, payload, context);
167
+ return { httpStatus: statusCode, body: payload ?? { success: true } };
168
+ }
169
+ }