arc-1 0.6.0 → 0.6.2

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 (90) hide show
  1. package/README.md +25 -26
  2. package/dist/adt/btp.d.ts +19 -12
  3. package/dist/adt/btp.d.ts.map +1 -1
  4. package/dist/adt/btp.js +48 -35
  5. package/dist/adt/btp.js.map +1 -1
  6. package/dist/adt/client.d.ts +20 -2
  7. package/dist/adt/client.d.ts.map +1 -1
  8. package/dist/adt/client.js +65 -2
  9. package/dist/adt/client.js.map +1 -1
  10. package/dist/adt/codeintel.d.ts.map +1 -1
  11. package/dist/adt/codeintel.js +1 -10
  12. package/dist/adt/codeintel.js.map +1 -1
  13. package/dist/adt/config.d.ts +2 -0
  14. package/dist/adt/config.d.ts.map +1 -1
  15. package/dist/adt/config.js +2 -0
  16. package/dist/adt/config.js.map +1 -1
  17. package/dist/adt/crud.d.ts +7 -0
  18. package/dist/adt/crud.d.ts.map +1 -1
  19. package/dist/adt/crud.js +35 -3
  20. package/dist/adt/crud.js.map +1 -1
  21. package/dist/adt/ddic-xml.d.ts +78 -0
  22. package/dist/adt/ddic-xml.d.ts.map +1 -0
  23. package/dist/adt/ddic-xml.js +203 -0
  24. package/dist/adt/ddic-xml.js.map +1 -0
  25. package/dist/adt/devtools.d.ts +30 -7
  26. package/dist/adt/devtools.d.ts.map +1 -1
  27. package/dist/adt/devtools.js +215 -76
  28. package/dist/adt/devtools.js.map +1 -1
  29. package/dist/adt/diagnostics.d.ts.map +1 -1
  30. package/dist/adt/diagnostics.js +105 -184
  31. package/dist/adt/diagnostics.js.map +1 -1
  32. package/dist/adt/errors.d.ts +12 -0
  33. package/dist/adt/errors.d.ts.map +1 -1
  34. package/dist/adt/errors.js +42 -0
  35. package/dist/adt/errors.js.map +1 -1
  36. package/dist/adt/features.d.ts.map +1 -1
  37. package/dist/adt/features.js +23 -3
  38. package/dist/adt/features.js.map +1 -1
  39. package/dist/adt/flp.d.ts +43 -0
  40. package/dist/adt/flp.d.ts.map +1 -0
  41. package/dist/adt/flp.js +213 -0
  42. package/dist/adt/flp.js.map +1 -0
  43. package/dist/adt/http.d.ts +2 -0
  44. package/dist/adt/http.d.ts.map +1 -1
  45. package/dist/adt/http.js +162 -4
  46. package/dist/adt/http.js.map +1 -1
  47. package/dist/adt/transport.d.ts +48 -3
  48. package/dist/adt/transport.d.ts.map +1 -1
  49. package/dist/adt/transport.js +222 -32
  50. package/dist/adt/transport.js.map +1 -1
  51. package/dist/adt/types.d.ts +107 -0
  52. package/dist/adt/types.d.ts.map +1 -1
  53. package/dist/adt/ui5-repository.d.ts +19 -0
  54. package/dist/adt/ui5-repository.d.ts.map +1 -0
  55. package/dist/adt/ui5-repository.js +43 -0
  56. package/dist/adt/ui5-repository.js.map +1 -0
  57. package/dist/adt/xml-parser.d.ts +43 -1
  58. package/dist/adt/xml-parser.d.ts.map +1 -1
  59. package/dist/adt/xml-parser.js +208 -3
  60. package/dist/adt/xml-parser.js.map +1 -1
  61. package/dist/handlers/intent.d.ts +20 -6
  62. package/dist/handlers/intent.d.ts.map +1 -1
  63. package/dist/handlers/intent.js +1304 -104
  64. package/dist/handlers/intent.js.map +1 -1
  65. package/dist/handlers/schemas.d.ts +240 -0
  66. package/dist/handlers/schemas.d.ts.map +1 -1
  67. package/dist/handlers/schemas.js +201 -5
  68. package/dist/handlers/schemas.js.map +1 -1
  69. package/dist/handlers/tools.d.ts.map +1 -1
  70. package/dist/handlers/tools.js +339 -49
  71. package/dist/handlers/tools.js.map +1 -1
  72. package/dist/server/config.d.ts.map +1 -1
  73. package/dist/server/config.js +15 -3
  74. package/dist/server/config.js.map +1 -1
  75. package/dist/server/http.d.ts.map +1 -1
  76. package/dist/server/http.js +32 -3
  77. package/dist/server/http.js.map +1 -1
  78. package/dist/server/server.d.ts +1 -1
  79. package/dist/server/server.d.ts.map +1 -1
  80. package/dist/server/server.js +38 -6
  81. package/dist/server/server.js.map +1 -1
  82. package/dist/server/types.d.ts +2 -0
  83. package/dist/server/types.d.ts.map +1 -1
  84. package/dist/server/types.js +5 -3
  85. package/dist/server/types.js.map +1 -1
  86. package/dist/server/xsuaa.d.ts +13 -0
  87. package/dist/server/xsuaa.d.ts.map +1 -1
  88. package/dist/server/xsuaa.js +28 -2
  89. package/dist/server/xsuaa.js.map +1 -1
  90. package/package.json +19 -6
@@ -10,13 +10,16 @@
10
10
  * leaked to the LLM — only user-friendly error messages.
11
11
  */
12
12
  import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '../adt/codeintel.js';
13
- import { createObject, deleteObject, lockObject, safeUpdateSource, unlockObject } from '../adt/crud.js';
14
- import { activate, activateBatch, runAtcCheck, runUnitTests, syntaxCheck } from '../adt/devtools.js';
13
+ import { createObject, deleteObject, lockObject, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, } from '../adt/crud.js';
14
+ import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildPackageXml, buildServiceBindingXml, } from '../adt/ddic-xml.js';
15
+ import { activate, activateBatch, publishServiceBinding, runAtcCheck, runUnitTests, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
15
16
  import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
16
17
  import { AdtApiError, AdtNetworkError, AdtSafetyError, isNotFoundError } from '../adt/errors.js';
17
18
  import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
18
- import { checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
19
- import { createTransport, getTransport, listTransports, releaseTransport } from '../adt/transport.js';
19
+ import { addTileToGroup, createCatalog, createGroup, createTile, deleteCatalog, listCatalogs, listGroups, listTiles, } from '../adt/flp.js';
20
+ import { checkOperation, checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
21
+ import { createTransport, deleteTransport, getTransport, getTransportInfo, listTransports, reassignTransport, releaseTransport, releaseTransportRecursive, } from '../adt/transport.js';
22
+ import { getAppInfo } from '../adt/ui5-repository.js';
20
23
  import { validateAffHeader } from '../aff/validator.js';
21
24
  import { extractCdsElements } from '../context/cds-deps.js';
22
25
  import { compressCdsContext, compressContext } from '../context/compressor.js';
@@ -74,24 +77,132 @@ function textResult(text) {
74
77
  function errorResult(message) {
75
78
  return { content: [{ type: 'text', text: message }], isError: true };
76
79
  }
77
- /** Classify error type for audit logging */
80
+ // ─── Search Helpers ─────────────────────────────────────────────────
81
+ /**
82
+ * Transliterate non-ASCII characters in search queries.
83
+ * SAP object names are ASCII-only, so umlauts and accented characters
84
+ * never appear in object names. This prevents wasted searches with
85
+ * German terms like "*Schätzung*" that silently return empty results.
86
+ */
87
+ export function transliterateQuery(query) {
88
+ // Explicit German umlaut replacements (must come before NFD decomposition)
89
+ let result = query
90
+ .replace(/ä/g, 'AE')
91
+ .replace(/Ä/g, 'AE')
92
+ .replace(/ö/g, 'OE')
93
+ .replace(/Ö/g, 'OE')
94
+ .replace(/ü/g, 'UE')
95
+ .replace(/Ü/g, 'UE')
96
+ .replace(/ß/g, 'SS');
97
+ // General fallback: strip remaining diacritics (é→e, ñ→n, etc.)
98
+ result = result.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
99
+ return { normalized: result, changed: result !== query };
100
+ }
101
+ /**
102
+ * Detect if a search query looks like a field/column name rather than
103
+ * an object name. Field names are short, uppercase, and typically don't
104
+ * start with Z/Y (which are custom object prefixes).
105
+ */
106
+ export function looksLikeFieldName(query) {
107
+ // Wildcard patterns are object searches, not field names
108
+ if (query.includes('*'))
109
+ return false;
110
+ if (query.length === 0 || query.length > 15)
111
+ return false;
112
+ // Must be uppercase letters, digits, underscores only
113
+ if (!/^[A-Z0-9_]+$/.test(query))
114
+ return false;
115
+ // Z/Y prefix → more likely an object name
116
+ if (/^[ZY]/.test(query))
117
+ return false;
118
+ return true;
119
+ }
78
120
  /** Format error messages with LLM-friendly remediation hints */
79
121
  function formatErrorForLLM(err, message, _tool, args) {
80
122
  if (err instanceof AdtApiError) {
123
+ // Append additional SAP messages (line numbers, secondary errors) if available
124
+ const enriched = enrichWithSapDetails(err, message);
81
125
  if (err.isNotFound) {
82
126
  const name = String(args.name ?? '');
83
127
  const type = String(args.type ?? '');
84
- return `${message}\n\nHint: Object "${name}" (type ${type}) was not found. Use SAPSearch with query "${name}" to verify the name exists and check the correct type.`;
128
+ return `${enriched}\n\nHint: Object "${name}" (type ${type}) was not found. Use SAPSearch with query "${name}" to verify the name exists and check the correct type.`;
85
129
  }
86
130
  if (err.isUnauthorized || err.isForbidden) {
87
- return `${message}\n\nHint: Authorization error. Check SAP_CLIENT (default: '100'), SAP_USER, and SAP_PASSWORD. The configured SAP user may lack permissions for this object.`;
131
+ return `${enriched}\n\nHint: Authorization error. Check SAP_CLIENT (default: '100'), SAP_USER, and SAP_PASSWORD. The configured SAP user may lack permissions for this object.`;
132
+ }
133
+ // Transport / corrNr specific hints
134
+ const transportHint = getTransportHint(err);
135
+ if (transportHint) {
136
+ return `${enriched}\n\nHint: ${transportHint}`;
137
+ }
138
+ // Server errors (500, 502, 503, etc.)
139
+ if (err.isServerError) {
140
+ return `${enriched}\n\nHint: SAP application server error (${err.statusCode}). This is often transient — wait 10-30 seconds and retry. If the error persists, check SAPDiagnose(action="dumps") for short dumps, or verify the SAP system is responding via SAPRead(type="SYSTEM").`;
88
141
  }
142
+ return enriched;
89
143
  }
90
144
  if (err instanceof AdtNetworkError) {
91
145
  return `${message}\n\nHint: Cannot reach the SAP system. This is a connectivity issue, not a usage error.`;
92
146
  }
93
147
  return message;
94
148
  }
149
+ /** Enrich error message with additional SAP XML diagnostic detail (extra messages, properties) */
150
+ function enrichWithSapDetails(err, message) {
151
+ if (!err.responseBody)
152
+ return message;
153
+ const extraMessages = AdtApiError.extractAllMessages(err.responseBody);
154
+ const props = AdtApiError.extractProperties(err.responseBody);
155
+ const parts = [message];
156
+ if (extraMessages.length > 0) {
157
+ parts.push(`\nAdditional detail:\n${extraMessages.map((m) => ` - ${m}`).join('\n')}`);
158
+ }
159
+ // Surface line/column info from properties if present
160
+ const lineInfo = props.LINE || props['T100KEY-NO'];
161
+ if (lineInfo || Object.keys(props).length > 0) {
162
+ const propStr = Object.entries(props)
163
+ .slice(0, 5) // Limit to avoid overwhelming output
164
+ .map(([k, v]) => `${k}=${v}`)
165
+ .join(', ');
166
+ if (propStr)
167
+ parts.push(`Properties: ${propStr}`);
168
+ }
169
+ return parts.join('\n');
170
+ }
171
+ /** Detect transport/corrNr failure signatures and return a remediation hint, or undefined if not transport-related. */
172
+ function getTransportHint(err) {
173
+ const body = (err.responseBody ?? '').toLowerCase();
174
+ // Use the clean SAP error message, NOT err.message which includes the URL path.
175
+ // The URL path contains `corrNr=<id>` when a transport IS provided, causing false positives
176
+ // if we check for "corrnr" in the full message string.
177
+ const cleanMsg = AdtApiError.extractCleanMessage(err.responseBody ?? '').toLowerCase();
178
+ const combined = `${cleanMsg} ${body}`;
179
+ // Missing or invalid transport/correction number
180
+ if (combined.includes('correction number') ||
181
+ combined.includes('corrnr') ||
182
+ (combined.includes('transport request') &&
183
+ (combined.includes('missing') || combined.includes('required') || combined.includes('invalid')))) {
184
+ return 'A transport/correction number is required but was not provided or is invalid. Provide an explicit "transport" parameter with a valid transport request ID, or check SE09 in SAP GUI that an open transport exists for your user and target package.';
185
+ }
186
+ // Transport not found or not modifiable
187
+ if (combined.includes('e070') ||
188
+ (combined.includes('transport') &&
189
+ (combined.includes('not found') || combined.includes('does not exist') || combined.includes('not modifiable')))) {
190
+ return 'The specified transport request was not found or is not modifiable. Verify the transport ID in SE09, ensure it is not yet released, and that it belongs to the correct user and target package.';
191
+ }
192
+ // Package / transport layer mismatch
193
+ if (combined.includes('transport layer') ||
194
+ (combined.includes('package') &&
195
+ combined.includes('transport') &&
196
+ (combined.includes('mismatch') || combined.includes('not assigned') || combined.includes('no transport layer')))) {
197
+ return 'The target package has no transport layer or a transport layer mismatch. Check that the package is configured for transport in SE80/TDEVC, or use a local package ($TMP) if no transport is needed.';
198
+ }
199
+ // Authorization for transport operations
200
+ if (combined.includes('s_transprt') ||
201
+ (combined.includes('transport') && (combined.includes('no authorization') || combined.includes('not authorized')))) {
202
+ return 'The SAP user lacks transport authorization (S_TRANSPRT). Contact your SAP basis administrator to grant the required transport permissions.';
203
+ }
204
+ return undefined;
205
+ }
95
206
  function classifyError(err) {
96
207
  if (err instanceof AdtApiError)
97
208
  return 'AdtApiError';
@@ -292,20 +403,26 @@ async function handleSAPRead(client, args, cachingLayer) {
292
403
  if (isBtpSystem() && BTP_HINTS[type]) {
293
404
  return errorResult(BTP_HINTS[type]);
294
405
  }
295
- // Helper: get source with cache support
406
+ // Helper: get source with cache support, returns cache hit status
296
407
  const cachedGet = async (objType, objName, fetcher) => {
297
408
  if (!cachingLayer)
298
- return fetcher();
299
- const { source } = await cachingLayer.getSource(objType, objName, fetcher);
300
- return source;
409
+ return { source: await fetcher(), cacheHit: false };
410
+ const { source, hit } = await cachingLayer.getSource(objType, objName, fetcher);
411
+ return { source, cacheHit: hit };
412
+ };
413
+ /** Prepend [cached] indicator when result came from cache */
414
+ const cachedTextResult = (source, cacheHit) => {
415
+ return textResult(cacheHit ? `[cached]\n${source}` : source);
301
416
  };
302
417
  // Structured format is only supported for CLAS type
303
418
  if (args.format === 'structured' && type !== 'CLAS') {
304
419
  return errorResult('The "structured" format is only supported for CLAS type. Other types return text format.');
305
420
  }
306
421
  switch (type) {
307
- case 'PROG':
308
- return textResult(await cachedGet('PROG', name, () => client.getProgram(name)));
422
+ case 'PROG': {
423
+ const { source, cacheHit } = await cachedGet('PROG', name, () => client.getProgram(name));
424
+ return cachedTextResult(source, cacheHit);
425
+ }
309
426
  case 'CLAS': {
310
427
  // Structured format: return JSON with metadata + decomposed source
311
428
  if (args.format === 'structured') {
@@ -314,8 +431,8 @@ async function handleSAPRead(client, args, cachingLayer) {
314
431
  }
315
432
  const methodParam = args.method;
316
433
  if (methodParam && !args.include) {
317
- // Method-level read — fetch full source then extract
318
- const fullSource = await cachedGet('CLAS', name, () => client.getClass(name));
434
+ // Method-level read — fetch full source then extract (no cache indicator for derived results)
435
+ const { source: fullSource } = await cachedGet('CLAS', name, () => client.getClass(name));
319
436
  const abaplintVer = cachedFeatures?.abapRelease
320
437
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
321
438
  : undefined;
@@ -331,12 +448,15 @@ async function handleSAPRead(client, args, cachingLayer) {
331
448
  }
332
449
  // Only cache the full merged source (no include param), not individual includes
333
450
  if (!args.include) {
334
- return textResult(await cachedGet('CLAS', name, () => client.getClass(name)));
451
+ const { source, cacheHit } = await cachedGet('CLAS', name, () => client.getClass(name));
452
+ return cachedTextResult(source, cacheHit);
335
453
  }
336
454
  return textResult(await client.getClass(name, args.include));
337
455
  }
338
- case 'INTF':
339
- return textResult(await cachedGet('INTF', name, () => client.getInterface(name)));
456
+ case 'INTF': {
457
+ const { source, cacheHit } = await cachedGet('INTF', name, () => client.getInterface(name));
458
+ return cachedTextResult(source, cacheHit);
459
+ }
340
460
  case 'FUNC': {
341
461
  let group = String(args.group ?? '');
342
462
  if (!group) {
@@ -349,7 +469,8 @@ async function handleSAPRead(client, args, cachingLayer) {
349
469
  }
350
470
  group = resolved;
351
471
  }
352
- return textResult(await cachedGet('FUNC', name, () => client.getFunction(group, name)));
472
+ const { source, cacheHit } = await cachedGet('FUNC', name, () => client.getFunction(group, name));
473
+ return cachedTextResult(source, cacheHit);
353
474
  }
354
475
  case 'FUGR': {
355
476
  const expand = Boolean(args.expand_includes);
@@ -374,22 +495,34 @@ async function handleSAPRead(client, args, cachingLayer) {
374
495
  const fg = await client.getFunctionGroup(name);
375
496
  return textResult(JSON.stringify(fg, null, 2));
376
497
  }
377
- case 'INCL':
378
- return textResult(await cachedGet('INCL', name, () => client.getInclude(name)));
498
+ case 'INCL': {
499
+ const { source, cacheHit } = await cachedGet('INCL', name, () => client.getInclude(name));
500
+ return cachedTextResult(source, cacheHit);
501
+ }
379
502
  case 'DDLS': {
380
- const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
503
+ const { source: ddlSource, cacheHit } = await cachedGet('DDLS', name, () => client.getDdls(name));
504
+ if (ddlSource.trim() === '') {
505
+ return textResult(`DDLS ${name} exists in the object directory but has no source code stored. ` +
506
+ `The DDL source may need to be written via SAPWrite(action="create" or "update", type="DDLS", name="${name}", source="...").`);
507
+ }
381
508
  if (args.include?.toLowerCase() === 'elements') {
509
+ // Elements extraction is derived from source — no cache indicator
382
510
  return textResult(extractCdsElements(ddlSource, name));
383
511
  }
384
- return textResult(ddlSource);
512
+ return cachedTextResult(ddlSource, cacheHit);
513
+ }
514
+ case 'BDEF': {
515
+ const { source, cacheHit } = await cachedGet('BDEF', name, () => client.getBdef(name));
516
+ return cachedTextResult(source, cacheHit);
517
+ }
518
+ case 'SRVD': {
519
+ const { source, cacheHit } = await cachedGet('SRVD', name, () => client.getSrvd(name));
520
+ return cachedTextResult(source, cacheHit);
385
521
  }
386
- case 'BDEF':
387
- return textResult(await cachedGet('BDEF', name, () => client.getBdef(name)));
388
- case 'SRVD':
389
- return textResult(await cachedGet('SRVD', name, () => client.getSrvd(name)));
390
522
  case 'DDLX': {
391
523
  try {
392
- return textResult(await cachedGet('DDLX', name, () => client.getDdlx(name)));
524
+ const { source, cacheHit } = await cachedGet('DDLX', name, () => client.getDdlx(name));
525
+ return cachedTextResult(source, cacheHit);
393
526
  }
394
527
  catch (err) {
395
528
  if (isNotFoundError(err)) {
@@ -398,14 +531,22 @@ async function handleSAPRead(client, args, cachingLayer) {
398
531
  throw err;
399
532
  }
400
533
  }
401
- case 'SRVB':
402
- return textResult(await cachedGet('SRVB', name, () => client.getSrvb(name)));
403
- case 'TABL':
404
- return textResult(await cachedGet('TABL', name, () => client.getTable(name)));
405
- case 'VIEW':
406
- return textResult(await cachedGet('VIEW', name, () => client.getView(name)));
407
- case 'STRU':
408
- return textResult(await cachedGet('STRU', name, () => client.getStructure(name)));
534
+ case 'SRVB': {
535
+ const { source, cacheHit } = await cachedGet('SRVB', name, () => client.getSrvb(name));
536
+ return cachedTextResult(source, cacheHit);
537
+ }
538
+ case 'TABL': {
539
+ const { source, cacheHit } = await cachedGet('TABL', name, () => client.getTable(name));
540
+ return cachedTextResult(source, cacheHit);
541
+ }
542
+ case 'VIEW': {
543
+ const { source, cacheHit } = await cachedGet('VIEW', name, () => client.getView(name));
544
+ return cachedTextResult(source, cacheHit);
545
+ }
546
+ case 'STRU': {
547
+ const { source, cacheHit } = await cachedGet('STRU', name, () => client.getStructure(name));
548
+ return cachedTextResult(source, cacheHit);
549
+ }
409
550
  case 'DOMA': {
410
551
  const domain = await client.getDomain(name);
411
552
  return textResult(JSON.stringify(domain, null, 2));
@@ -431,6 +572,18 @@ async function handleSAPRead(client, args, cachingLayer) {
431
572
  }
432
573
  return textResult(JSON.stringify(tran, null, 2));
433
574
  }
575
+ case 'API_STATE': {
576
+ // Determine object type for URL construction — use explicit objectType, infer from name, or error
577
+ const explicitType = String(args.objectType ?? '').toUpperCase();
578
+ const inferredType = explicitType || inferObjectType(name);
579
+ if (!inferredType) {
580
+ return errorResult(`Cannot infer object type from name "${name}". Please specify objectType explicitly (e.g., objectType="CLAS", "INTF", "PROG", "TABL", "DDLS", "FUGR", "DOMA", "DTEL", "SRVD", "SRVB", "BDEF").`);
581
+ }
582
+ // Use raw URI (no name encoding) — getApiReleaseState encodes the full URI as a single path segment
583
+ const objectUri = objectUrlForTypeRaw(inferredType, name);
584
+ const releaseState = await client.getApiReleaseState(objectUri);
585
+ return textResult(JSON.stringify(releaseState, null, 2));
586
+ }
434
587
  case 'TABLE_CONTENTS': {
435
588
  const maxRows = Number(args.maxRows ?? 100);
436
589
  const data = await client.getTableContents(name, maxRows, args.sqlFilter);
@@ -474,24 +627,81 @@ async function handleSAPRead(client, args, cachingLayer) {
474
627
  const components = await client.getInstalledComponents();
475
628
  return textResult(JSON.stringify(components, null, 2));
476
629
  }
477
- case 'MESSAGES':
478
- return textResult(await client.getMessages(name));
630
+ case 'MESSAGES': {
631
+ try {
632
+ const mcInfo = await client.getMessageClassInfo(name);
633
+ return textResult(JSON.stringify(mcInfo, null, 2));
634
+ }
635
+ catch {
636
+ // Fall back to legacy endpoint if messageclass endpoint unavailable
637
+ return textResult(await client.getMessages(name));
638
+ }
639
+ }
479
640
  case 'TEXT_ELEMENTS':
480
641
  return textResult(await client.getTextElements(name));
481
642
  case 'VARIANTS':
482
643
  return textResult(await client.getVariants(name));
644
+ case 'BSP': {
645
+ if (cachedFeatures?.ui5 && !cachedFeatures.ui5.available) {
646
+ return errorResult('UI5/Fiori BSP Filestore is not available on this SAP system. ' +
647
+ 'Run SAPManage(action="probe") to verify feature availability.');
648
+ }
649
+ const include = args.include;
650
+ if (!name) {
651
+ // List all BSP apps (optional search via query param not used here since name is empty)
652
+ const apps = await client.listBspApps();
653
+ return textResult(JSON.stringify(apps, null, 2));
654
+ }
655
+ if (!include) {
656
+ // Browse root structure of the app
657
+ return textResult(JSON.stringify(await client.getBspAppStructure(name), null, 2));
658
+ }
659
+ // If include contains a dot, treat as file read; otherwise browse subfolder
660
+ if (include.includes('.')) {
661
+ return textResult(await client.getBspFileContent(name, include));
662
+ }
663
+ return textResult(JSON.stringify(await client.getBspAppStructure(name, `/${include}`), null, 2));
664
+ }
665
+ case 'BSP_DEPLOY': {
666
+ if (cachedFeatures?.ui5repo && !cachedFeatures.ui5repo.available) {
667
+ return errorResult('ABAP Repository OData Service is not available on this SAP system. ' +
668
+ 'Run SAPManage(action="probe") to verify feature availability.');
669
+ }
670
+ if (!name) {
671
+ return errorResult('BSP_DEPLOY requires a name parameter (e.g., name="ZAPP_BOOKING").');
672
+ }
673
+ const info = await getAppInfo(client.http, client.safety, name);
674
+ if (!info) {
675
+ return textResult(`App "${name}" not found in ABAP Repository.`);
676
+ }
677
+ return textResult(JSON.stringify(info, null, 2));
678
+ }
679
+ case 'INACTIVE_OBJECTS': {
680
+ try {
681
+ const objects = await client.getInactiveObjects();
682
+ return textResult(JSON.stringify({ count: objects.length, objects }, null, 2));
683
+ }
684
+ catch (err) {
685
+ if (isNotFoundError(err)) {
686
+ return textResult('Inactive objects listing is not available on this SAP system ' +
687
+ '(the /sap/bc/adt/activation/inactive endpoint returned 404). ' +
688
+ 'Use SAPDiagnose(action="syntax", type="...", name="...") to check specific objects instead.');
689
+ }
690
+ throw err;
691
+ }
692
+ }
483
693
  default:
484
- return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS. ` +
694
+ return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS, BSP, BSP_DEPLOY, API_STATE, INACTIVE_OBJECTS. ` +
485
695
  'Tip: Map objectType from SAPSearch results by dropping the slash suffix (e.g., DDLS/DF → type="DDLS", CLAS/OC → type="CLAS", PROG/P → type="PROG"). ' +
486
696
  'Do not pass a URI — use the "type" and "name" parameters instead.');
487
697
  }
488
698
  }
489
699
  async function handleSAPSearch(client, args) {
490
- const query = String(args.query ?? '');
700
+ const rawQuery = String(args.query ?? '');
491
701
  const maxResults = Number(args.maxResults ?? 100);
492
702
  const searchType = String(args.searchType ?? 'object');
493
703
  if (searchType === 'source_code') {
494
- // If probe already determined textSearch is unavailable, return the precise reason
704
+ // Source code search: do NOT transliterate source can contain umlauts in strings/comments
495
705
  if (cachedFeatures?.textSearch && !cachedFeatures.textSearch.available) {
496
706
  return errorResult(`Source code search is not available on this SAP system. ${cachedFeatures.textSearch.reason ?? ''}` +
497
707
  `\nUse SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
@@ -499,7 +709,7 @@ async function handleSAPSearch(client, args) {
499
709
  const objectType = args.objectType;
500
710
  const packageName = args.packageName;
501
711
  try {
502
- const results = await client.searchSource(query, maxResults, objectType, packageName);
712
+ const results = await client.searchSource(rawQuery, maxResults, objectType, packageName);
503
713
  return textResult(JSON.stringify(results, null, 2));
504
714
  }
505
715
  catch (err) {
@@ -514,8 +724,25 @@ async function handleSAPSearch(client, args) {
514
724
  throw err;
515
725
  }
516
726
  }
727
+ // Object search: transliterate non-ASCII (SAP object names are ASCII-only)
728
+ const { normalized: query, changed: wasTransliterated } = transliterateQuery(rawQuery);
729
+ const transliterationNote = wasTransliterated
730
+ ? `Note: Query contained non-ASCII characters. Transliterated "${rawQuery}" → "${query}" (SAP object names are ASCII-only).\n\n`
731
+ : '';
517
732
  const results = await client.searchObject(query, maxResults);
518
- return textResult(JSON.stringify(results, null, 2));
733
+ if (Array.isArray(results) && results.length === 0) {
734
+ let hint = '[]' +
735
+ '\n\n' +
736
+ transliterationNote +
737
+ 'No objects found. If searching for custom objects, try Z* or Y* prefixes (e.g., "Z*ESTIM*"). ' +
738
+ 'If you already found objects in a package, use SAPRead with type=DEVC to list all package contents instead of more searches.';
739
+ if (looksLikeFieldName(query)) {
740
+ const stripped = query.replace(/\*/g, '');
741
+ hint += `\nThis looks like a field/column name. Use SAPQuery("SELECT fieldname, rollname, domname FROM dd03l WHERE fieldname = '${stripped}'") or SAPRead(type='DDLS', include='elements') to find fields.`;
742
+ }
743
+ return textResult(hint);
744
+ }
745
+ return textResult(transliterationNote + JSON.stringify(results, null, 2));
519
746
  }
520
747
  async function handleSAPQuery(client, args) {
521
748
  const sql = String(args.sql ?? '');
@@ -616,6 +843,187 @@ function buildLintConfigOptions(config, ruleOverrides) {
616
843
  };
617
844
  }
618
845
  // ─── Object Creation XML ─────────────────────────────────────────────
846
+ const DOMAIN_V2_CONTENT_TYPE = 'application/vnd.sap.adt.domains.v2+xml; charset=utf-8';
847
+ const DATAELEMENT_V2_CONTENT_TYPE = 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8';
848
+ const SERVICEBINDING_V2_CONTENT_TYPE = 'application/vnd.sap.adt.businessservices.servicebinding.v2+xml; charset=utf-8';
849
+ const BDEF_CONTENT_TYPE = 'application/vnd.sap.adt.blues.v1+xml';
850
+ const MESSAGECLASS_CONTENT_TYPE = 'application/vnd.sap.adt.mc.messageclass+xml';
851
+ function isMetadataWriteType(type) {
852
+ return type === 'DOMA' || type === 'DTEL' || type === 'MSAG' || type === 'SRVB';
853
+ }
854
+ /** Types that require a specific vendor content type for creation (not application/*) */
855
+ function needsVendorContentType(type) {
856
+ return type === 'DOMA' || type === 'DTEL' || type === 'BDEF' || type === 'MSAG';
857
+ }
858
+ /** Content type used for create POST */
859
+ function createContentTypeForType(type) {
860
+ // SRVB creation works with wildcard content type; updates use vendor v2 type.
861
+ if (type === 'SRVB')
862
+ return 'application/*';
863
+ return needsVendorContentType(type) ? vendorContentTypeForType(type) : 'application/*';
864
+ }
865
+ /**
866
+ * Check if a DTEL create has properties that SAP ignores on POST but accepts on PUT.
867
+ * SAP's DTEL POST only stores the shell (name, description, package, typeKind, typeName, dataType, length).
868
+ * Labels, searchHelp, setGetParameter, etc. require a follow-up PUT to take effect.
869
+ */
870
+ function dtelNeedsPostCreateUpdate(props) {
871
+ return Boolean(props.shortLabel ||
872
+ props.mediumLabel ||
873
+ props.longLabel ||
874
+ props.headingLabel ||
875
+ props.searchHelp ||
876
+ props.searchHelpParameter ||
877
+ props.setGetParameter ||
878
+ props.defaultComponentName ||
879
+ props.changeDocument);
880
+ }
881
+ function vendorContentTypeForType(type) {
882
+ switch (type) {
883
+ case 'DOMA':
884
+ return DOMAIN_V2_CONTENT_TYPE;
885
+ case 'DTEL':
886
+ return DATAELEMENT_V2_CONTENT_TYPE;
887
+ case 'SRVB':
888
+ return SERVICEBINDING_V2_CONTENT_TYPE;
889
+ case 'BDEF':
890
+ return BDEF_CONTENT_TYPE;
891
+ case 'MSAG':
892
+ return MESSAGECLASS_CONTENT_TYPE;
893
+ default:
894
+ // Wildcard lets the SAP server resolve the correct handler.
895
+ // Sending 'application/xml' causes 415 on DDL-based endpoints
896
+ // (DDLS, SRVD, DDLX) whose resource classes reject that literal type.
897
+ return 'application/*';
898
+ }
899
+ }
900
+ function toBoolean(value) {
901
+ if (typeof value === 'boolean')
902
+ return value;
903
+ if (typeof value === 'number')
904
+ return value !== 0;
905
+ if (typeof value === 'string') {
906
+ const normalized = value.trim().toLowerCase();
907
+ if (normalized === 'true')
908
+ return true;
909
+ if (normalized === 'false')
910
+ return false;
911
+ }
912
+ return undefined;
913
+ }
914
+ function getMetadataWriteProperties(input) {
915
+ const props = {
916
+ dataType: input.dataType,
917
+ length: input.length,
918
+ decimals: input.decimals,
919
+ outputLength: input.outputLength,
920
+ conversionExit: input.conversionExit,
921
+ signExists: input.signExists,
922
+ lowercase: input.lowercase,
923
+ fixedValues: input.fixedValues,
924
+ valueTable: input.valueTable,
925
+ typeKind: input.typeKind,
926
+ typeName: input.typeName,
927
+ domainName: input.domainName,
928
+ shortLabel: input.shortLabel,
929
+ mediumLabel: input.mediumLabel,
930
+ longLabel: input.longLabel,
931
+ headingLabel: input.headingLabel,
932
+ searchHelp: input.searchHelp,
933
+ searchHelpParameter: input.searchHelpParameter,
934
+ setGetParameter: input.setGetParameter,
935
+ defaultComponentName: input.defaultComponentName,
936
+ changeDocument: input.changeDocument,
937
+ messages: input.messages,
938
+ serviceDefinition: input.serviceDefinition,
939
+ bindingType: input.bindingType,
940
+ category: input.category,
941
+ version: input.version,
942
+ };
943
+ return props;
944
+ }
945
+ /**
946
+ * Fetch existing DDIC metadata and merge with provided properties.
947
+ * This ensures that updating a single field (e.g., shortLabel) doesn't
948
+ * reset other fields (e.g., dataType, typeKind) to defaults, since
949
+ * DDIC updates are full-XML-replace operations.
950
+ *
951
+ * Internal _description and _package fields carry the existing values
952
+ * for the caller to use as fallbacks.
953
+ */
954
+ function normalizeSrvbCategory(value) {
955
+ if (value === '0' || value === 0 || value === 'UI')
956
+ return '0';
957
+ if (value === '1' || value === 1 || value === 'Web API')
958
+ return '1';
959
+ return undefined;
960
+ }
961
+ async function mergeMetadataWriteProperties(client, type, name, provided) {
962
+ try {
963
+ if (type === 'MSAG') {
964
+ const existing = await client.getMessageClassInfo(name);
965
+ return {
966
+ _description: existing.description,
967
+ _package: existing.package,
968
+ messages: provided.messages ?? existing.messages,
969
+ };
970
+ }
971
+ if (type === 'DOMA') {
972
+ const existing = await client.getDomain(name);
973
+ return {
974
+ _description: existing.description,
975
+ _package: existing.package,
976
+ dataType: provided.dataType ?? existing.dataType,
977
+ length: provided.length ?? existing.length,
978
+ decimals: provided.decimals ?? existing.decimals,
979
+ outputLength: provided.outputLength ?? existing.outputLength,
980
+ conversionExit: provided.conversionExit ?? existing.conversionExit,
981
+ signExists: provided.signExists ?? existing.signExists,
982
+ lowercase: provided.lowercase ?? existing.lowercase,
983
+ fixedValues: provided.fixedValues ?? existing.fixedValues,
984
+ valueTable: provided.valueTable ?? existing.valueTable,
985
+ };
986
+ }
987
+ if (type === 'DTEL') {
988
+ const existing = await client.getDataElement(name);
989
+ return {
990
+ _description: existing.description,
991
+ _package: existing.package,
992
+ dataType: provided.dataType ?? existing.dataType,
993
+ length: provided.length ?? existing.length,
994
+ decimals: provided.decimals ?? existing.decimals,
995
+ typeKind: provided.typeKind ?? existing.typeKind,
996
+ typeName: provided.typeName ?? existing.typeName,
997
+ domainName: provided.domainName ?? existing.typeName, // DTEL stores domain in typeName
998
+ shortLabel: provided.shortLabel ?? existing.shortLabel,
999
+ mediumLabel: provided.mediumLabel ?? existing.mediumLabel,
1000
+ longLabel: provided.longLabel ?? existing.longLabel,
1001
+ headingLabel: provided.headingLabel ?? existing.headingLabel,
1002
+ searchHelp: provided.searchHelp ?? existing.searchHelp,
1003
+ searchHelpParameter: provided.searchHelpParameter,
1004
+ setGetParameter: provided.setGetParameter,
1005
+ defaultComponentName: provided.defaultComponentName ?? existing.defaultComponentName,
1006
+ changeDocument: provided.changeDocument,
1007
+ };
1008
+ }
1009
+ if (type === 'SRVB') {
1010
+ const existingRaw = await client.getSrvb(name);
1011
+ const existing = JSON.parse(existingRaw);
1012
+ return {
1013
+ _description: existing.description,
1014
+ _package: existing.package,
1015
+ serviceDefinition: provided.serviceDefinition ?? existing.serviceDefinition,
1016
+ bindingType: provided.bindingType ?? existing.bindingType,
1017
+ category: provided.category ?? normalizeSrvbCategory(existing.bindingCategory),
1018
+ version: provided.version ?? existing.serviceVersion,
1019
+ };
1020
+ }
1021
+ }
1022
+ catch {
1023
+ // If we can't read existing metadata (e.g., object is new/inactive), fall through
1024
+ }
1025
+ return provided;
1026
+ }
619
1027
  /**
620
1028
  * Build the type-specific XML body for ADT object creation.
621
1029
  *
@@ -623,7 +1031,101 @@ function buildLintConfigOptions(config, ruleOverrides) {
623
1031
  * Using a generic body (e.g. adtcore:objectReferences) returns 400:
624
1032
  * "System expected the element '{http://www.sap.com/adt/programs/programs}abapProgram'"
625
1033
  */
626
- export function buildCreateXml(type, name, pkg, description) {
1034
+ // ─── CDS Pre-Write Validation ──────────────────────────────────────
1035
+ /** Common CDS reserved/function keywords that cause silent DDL save failures when used as field names */
1036
+ const CDS_RESERVED_KEYWORDS = new Set([
1037
+ 'position',
1038
+ 'value',
1039
+ 'type',
1040
+ 'data',
1041
+ 'timestamp',
1042
+ 'language',
1043
+ 'text',
1044
+ 'source',
1045
+ 'target',
1046
+ 'name',
1047
+ 'description',
1048
+ 'concat',
1049
+ 'replace',
1050
+ 'substring',
1051
+ 'length',
1052
+ 'left',
1053
+ 'right',
1054
+ 'round',
1055
+ 'abs',
1056
+ 'floor',
1057
+ 'ceiling',
1058
+ 'division',
1059
+ 'mod',
1060
+ 'case',
1061
+ 'when',
1062
+ 'then',
1063
+ 'else',
1064
+ 'end',
1065
+ 'cast',
1066
+ 'coalesce',
1067
+ 'uuid',
1068
+ ]);
1069
+ /**
1070
+ * Guard CDS syntax against known version-dependent features.
1071
+ * Returns an error result if the source uses unsupported syntax, or undefined to proceed.
1072
+ * Best-effort: if cachedFeatures is not available (no probe yet), always proceeds.
1073
+ */
1074
+ function guardCdsSyntax(type, source, features) {
1075
+ if (type !== 'DDLS' || !source)
1076
+ return undefined;
1077
+ // Guard: "define table entity" requires ABAP Cloud (BTP) or SAP_BASIS >= 757
1078
+ if (/\bdefine\s+table\s+(entity|function)\b/i.test(source)) {
1079
+ const release = features?.abapRelease;
1080
+ const isBtp = features?.systemType === 'btp';
1081
+ if (!isBtp && release) {
1082
+ const releaseNum = Number.parseInt(release.replace(/\D/g, ''), 10);
1083
+ if (releaseNum > 0 && releaseNum < 757) {
1084
+ return errorResult(`"define table entity" syntax requires ABAP Cloud (BTP) or S/4HANA on-premise with SAP_BASIS >= 757. ` +
1085
+ `This system reports SAP_BASIS ${release}. ` +
1086
+ `Use DDIC transparent tables (SAPWrite type="TABL" or SE11) + CDS view entities ("define [root] view entity") instead.`);
1087
+ }
1088
+ }
1089
+ }
1090
+ // Advisory: warn about CDS reserved keywords used as field names
1091
+ const keywordWarning = warnCdsReservedKeywords(source);
1092
+ if (keywordWarning) {
1093
+ // Non-blocking — return undefined to proceed, but the warning will be
1094
+ // appended to the success message by the caller if needed.
1095
+ // For now we return it as an advisory error only when the keyword is
1096
+ // highly likely to cause issues (position is the most common).
1097
+ // We don't block the write — just append it as advisory context.
1098
+ }
1099
+ return undefined;
1100
+ }
1101
+ /**
1102
+ * Detect CDS reserved keywords used as field names in DDL source.
1103
+ * Returns a warning string listing suspicious field names, or undefined if none found.
1104
+ */
1105
+ export function warnCdsReservedKeywords(source) {
1106
+ // Extract field-name-like tokens: lines inside { } that define fields
1107
+ // Pattern: whitespace + identifier + colon (field definitions)
1108
+ const fieldNames = [];
1109
+ const braceStart = source.indexOf('{');
1110
+ const braceEnd = source.lastIndexOf('}');
1111
+ if (braceStart === -1 || braceEnd === -1)
1112
+ return undefined;
1113
+ const body = source.slice(braceStart + 1, braceEnd);
1114
+ // Match field definitions: leading whitespace, optional "key", then identifier before ":"
1115
+ const fieldPattern = /^\s*(?:key\s+)?(\w+)\s*:/gim;
1116
+ let match;
1117
+ while ((match = fieldPattern.exec(body)) !== null) {
1118
+ const fieldName = match[1]?.toLowerCase();
1119
+ if (fieldName && CDS_RESERVED_KEYWORDS.has(fieldName)) {
1120
+ fieldNames.push(match[1]);
1121
+ }
1122
+ }
1123
+ if (fieldNames.length === 0)
1124
+ return undefined;
1125
+ return (`Warning: field name(s) ${fieldNames.map((f) => `'${f}'`).join(', ')} may be CDS reserved keywords. ` +
1126
+ `If the DDL save fails with a generic syntax error, rename them (e.g., 'position' → 'playing_position', 'type' → 'obj_type').`);
1127
+ }
1128
+ export function buildCreateXml(type, name, pkg, description, properties) {
627
1129
  switch (type) {
628
1130
  case 'PROG':
629
1131
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -682,45 +1184,145 @@ export function buildCreateXml(type, name, pkg, description) {
682
1184
  adtcore:type="DDLS/DF"
683
1185
  adtcore:masterLanguage="EN"
684
1186
  adtcore:masterSystem="H00"
685
- adtcore:responsible="DEVELOPER">
1187
+ adtcore:responsible="DEVELOPER">
686
1188
  <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
687
1189
  </ddl:ddlSource>`;
1190
+ case 'TABL':
1191
+ // TABL creation also uses SAP's "blue" framework envelope, then source is written via /source/main.
1192
+ return `<?xml version="1.0" encoding="UTF-8"?>
1193
+ <blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
1194
+ xmlns:adtcore="http://www.sap.com/adt/core"
1195
+ adtcore:description="${escapeXml(description)}"
1196
+ adtcore:name="${escapeXml(name)}"
1197
+ adtcore:type="TABL/DT"
1198
+ adtcore:masterLanguage="EN"
1199
+ adtcore:masterSystem="H00"
1200
+ adtcore:responsible="DEVELOPER">
1201
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
1202
+ </blue:blueSource>`;
688
1203
  case 'BDEF':
1204
+ // BDEF uses SAP's "blue" framework — blue:blueSource with http://www.sap.com/wbobj/blue namespace.
1205
+ // Confirmed by vibing-steampunk (Go) and fr0ster (TypeScript) reference implementations.
689
1206
  return `<?xml version="1.0" encoding="UTF-8"?>
690
- <bdef:behaviorDefinition xmlns:bdef="http://www.sap.com/adt/bo/behaviordefinitions"
691
- xmlns:adtcore="http://www.sap.com/adt/core"
692
- adtcore:description="${escapeXml(description)}"
693
- adtcore:name="${escapeXml(name)}"
694
- adtcore:type="BDEF/BDO"
695
- adtcore:masterLanguage="EN"
696
- adtcore:masterSystem="H00"
697
- adtcore:responsible="DEVELOPER">
1207
+ <blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
1208
+ xmlns:adtcore="http://www.sap.com/adt/core"
1209
+ adtcore:description="${escapeXml(description)}"
1210
+ adtcore:name="${escapeXml(name)}"
1211
+ adtcore:type="BDEF/BDO"
1212
+ adtcore:masterLanguage="EN"
1213
+ adtcore:masterSystem="H00"
1214
+ adtcore:responsible="DEVELOPER">
698
1215
  <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
699
- </bdef:behaviorDefinition>`;
1216
+ </blue:blueSource>`;
700
1217
  case 'SRVD':
701
1218
  return `<?xml version="1.0" encoding="UTF-8"?>
702
- <srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/srvd/sources"
1219
+ <srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/srvdsources"
703
1220
  xmlns:adtcore="http://www.sap.com/adt/core"
704
1221
  adtcore:description="${escapeXml(description)}"
705
1222
  adtcore:name="${escapeXml(name)}"
706
1223
  adtcore:type="SRVD/SRV"
707
1224
  adtcore:masterLanguage="EN"
708
1225
  adtcore:masterSystem="H00"
709
- adtcore:responsible="DEVELOPER">
1226
+ adtcore:responsible="DEVELOPER"
1227
+ srvd:srvdSourceType="S">
710
1228
  <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
711
1229
  </srvd:srvdSource>`;
1230
+ case 'SRVB': {
1231
+ const serviceDefinition = String(properties?.serviceDefinition ?? '').trim();
1232
+ if (!serviceDefinition) {
1233
+ throw new Error('SRVB create/update requires "serviceDefinition" (referenced SRVD name).');
1234
+ }
1235
+ const categoryRaw = properties?.category;
1236
+ const category = categoryRaw === '1' || categoryRaw === 1 ? '1' : '0';
1237
+ const params = {
1238
+ name,
1239
+ description,
1240
+ package: pkg,
1241
+ serviceDefinition,
1242
+ bindingType: properties?.bindingType ? String(properties.bindingType) : undefined,
1243
+ category,
1244
+ version: properties?.version ? String(properties.version) : undefined,
1245
+ };
1246
+ return buildServiceBindingXml(params);
1247
+ }
712
1248
  case 'DDLX':
713
1249
  return `<?xml version="1.0" encoding="UTF-8"?>
714
- <ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/ddlx/sources"
1250
+ <ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/ddlxsources"
715
1251
  xmlns:adtcore="http://www.sap.com/adt/core"
716
1252
  adtcore:description="${escapeXml(description)}"
717
1253
  adtcore:name="${escapeXml(name)}"
718
1254
  adtcore:type="DDLX/EX"
719
1255
  adtcore:masterLanguage="EN"
720
1256
  adtcore:masterSystem="H00"
721
- adtcore:responsible="DEVELOPER">
1257
+ adtcore:responsible="DEVELOPER">
722
1258
  <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
723
1259
  </ddlx:ddlxSource>`;
1260
+ case 'DOMA': {
1261
+ const fixedValuesRaw = Array.isArray(properties?.fixedValues) ? properties.fixedValues : [];
1262
+ const fixedValues = fixedValuesRaw
1263
+ .filter((value) => typeof value === 'object' && value !== null)
1264
+ .map((value) => ({
1265
+ low: String(value.low ?? ''),
1266
+ high: value.high === undefined ? undefined : String(value.high),
1267
+ description: value.description === undefined ? undefined : String(value.description),
1268
+ }));
1269
+ const params = {
1270
+ name,
1271
+ description,
1272
+ package: pkg,
1273
+ dataType: String(properties?.dataType ?? 'CHAR'),
1274
+ length: properties?.length ?? 0,
1275
+ decimals: properties?.decimals,
1276
+ outputLength: properties?.outputLength,
1277
+ conversionExit: properties?.conversionExit ? String(properties.conversionExit) : undefined,
1278
+ signExists: toBoolean(properties?.signExists),
1279
+ lowercase: toBoolean(properties?.lowercase),
1280
+ fixedValues,
1281
+ valueTable: properties?.valueTable ? String(properties.valueTable) : undefined,
1282
+ };
1283
+ return buildDomainXml(params);
1284
+ }
1285
+ case 'DTEL': {
1286
+ const typeKindRaw = String(properties?.typeKind ?? '');
1287
+ const typeKind = typeKindRaw === 'domain' || typeKindRaw === 'predefinedAbapType' ? typeKindRaw : undefined;
1288
+ const params = {
1289
+ name,
1290
+ description,
1291
+ package: pkg,
1292
+ typeKind,
1293
+ typeName: properties?.typeName ? String(properties.typeName) : undefined,
1294
+ domainName: properties?.domainName ? String(properties.domainName) : undefined,
1295
+ dataType: properties?.dataType ? String(properties.dataType) : undefined,
1296
+ length: properties?.length,
1297
+ decimals: properties?.decimals,
1298
+ shortLabel: properties?.shortLabel ? String(properties.shortLabel) : undefined,
1299
+ mediumLabel: properties?.mediumLabel ? String(properties.mediumLabel) : undefined,
1300
+ longLabel: properties?.longLabel ? String(properties.longLabel) : undefined,
1301
+ headingLabel: properties?.headingLabel ? String(properties.headingLabel) : undefined,
1302
+ searchHelp: properties?.searchHelp ? String(properties.searchHelp) : undefined,
1303
+ searchHelpParameter: properties?.searchHelpParameter ? String(properties.searchHelpParameter) : undefined,
1304
+ setGetParameter: properties?.setGetParameter ? String(properties.setGetParameter) : undefined,
1305
+ defaultComponentName: properties?.defaultComponentName ? String(properties.defaultComponentName) : undefined,
1306
+ changeDocument: toBoolean(properties?.changeDocument),
1307
+ };
1308
+ return buildDataElementXml(params);
1309
+ }
1310
+ case 'MSAG': {
1311
+ const messagesRaw = Array.isArray(properties?.messages) ? properties.messages : [];
1312
+ const messages = messagesRaw
1313
+ .filter((m) => typeof m === 'object' && m !== null)
1314
+ .map((m) => ({
1315
+ number: String(m.number ?? ''),
1316
+ shortText: String(m.shortText ?? ''),
1317
+ }));
1318
+ const params = {
1319
+ name,
1320
+ description,
1321
+ package: pkg,
1322
+ messages: messages.length > 0 ? messages : undefined,
1323
+ };
1324
+ return buildMessageClassXml(params);
1325
+ }
724
1326
  default:
725
1327
  // Fallback — generic objectReferences using the correct URL for the type
726
1328
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -739,46 +1341,71 @@ function escapeXml(s) {
739
1341
  .replace(/>/g, '&gt;');
740
1342
  }
741
1343
  // ─── Object URL Mapping ──────────────────────────────────────────────
742
- /** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. */
743
- function objectUrlForType(type, name) {
744
- const encoded = encodeURIComponent(name);
1344
+ /** Base path for an object type. Returns path prefix without trailing name segment. */
1345
+ function objectBasePath(type) {
745
1346
  switch (type) {
746
1347
  case 'PROG':
747
- return `/sap/bc/adt/programs/programs/${encoded}`;
1348
+ return '/sap/bc/adt/programs/programs/';
748
1349
  case 'CLAS':
749
- return `/sap/bc/adt/oo/classes/${encoded}`;
1350
+ return '/sap/bc/adt/oo/classes/';
750
1351
  case 'INTF':
751
- return `/sap/bc/adt/oo/interfaces/${encoded}`;
1352
+ return '/sap/bc/adt/oo/interfaces/';
752
1353
  case 'FUNC':
753
- return `/sap/bc/adt/functions/groups/${encoded}`;
1354
+ return '/sap/bc/adt/functions/groups/';
754
1355
  case 'INCL':
755
- return `/sap/bc/adt/programs/includes/${encoded}`;
1356
+ return '/sap/bc/adt/programs/includes/';
756
1357
  case 'FUGR':
757
- return `/sap/bc/adt/functions/groups/${encoded}`;
1358
+ return '/sap/bc/adt/functions/groups/';
758
1359
  case 'DDLS':
759
- return `/sap/bc/adt/ddic/ddl/sources/${encoded}`;
1360
+ return '/sap/bc/adt/ddic/ddl/sources/';
760
1361
  case 'BDEF':
761
- return `/sap/bc/adt/bo/behaviordefinitions/${encoded}`;
1362
+ return '/sap/bc/adt/bo/behaviordefinitions/';
762
1363
  case 'SRVD':
763
- return `/sap/bc/adt/ddic/srvd/sources/${encoded}`;
1364
+ return '/sap/bc/adt/ddic/srvd/sources/';
764
1365
  case 'DDLX':
765
- return `/sap/bc/adt/ddic/ddlx/sources/${encoded}`;
1366
+ return '/sap/bc/adt/ddic/ddlx/sources/';
766
1367
  case 'SRVB':
767
- return `/sap/bc/adt/businessservices/bindings/${encoded}`;
1368
+ return '/sap/bc/adt/businessservices/bindings/';
768
1369
  case 'TABL':
769
- return `/sap/bc/adt/ddic/tables/${encoded}`;
1370
+ return '/sap/bc/adt/ddic/tables/';
770
1371
  case 'STRU':
771
- return `/sap/bc/adt/ddic/structures/${encoded}`;
1372
+ return '/sap/bc/adt/ddic/structures/';
772
1373
  case 'DOMA':
773
- return `/sap/bc/adt/ddic/domains/${encoded}`;
1374
+ return '/sap/bc/adt/ddic/domains/';
774
1375
  case 'DTEL':
775
- return `/sap/bc/adt/ddic/dataelements/${encoded}`;
1376
+ return '/sap/bc/adt/ddic/dataelements/';
1377
+ case 'MSAG':
1378
+ return '/sap/bc/adt/messageclass/';
1379
+ case 'DEVC':
1380
+ return '/sap/bc/adt/packages/';
776
1381
  case 'TRAN':
777
- return `/sap/bc/adt/vit/wb/object_type/trant/object_name/${encoded}`;
1382
+ return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
778
1383
  default:
779
- return `/sap/bc/adt/programs/programs/${encoded}`;
1384
+ return '/sap/bc/adt/programs/programs/';
780
1385
  }
781
1386
  }
1387
+ /** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. Name is URI-encoded. */
1388
+ function objectUrlForType(type, name) {
1389
+ return `${objectBasePath(type)}${encodeURIComponent(name)}`;
1390
+ }
1391
+ /** Infer SAP object type from naming conventions. Returns empty string if type cannot be determined. */
1392
+ function inferObjectType(name) {
1393
+ const upper = name.toUpperCase();
1394
+ if (upper.startsWith('IF_') || upper.startsWith('ZIF_') || upper.startsWith('YIF_'))
1395
+ return 'INTF';
1396
+ if (upper.startsWith('CL_') || upper.startsWith('ZCL_') || upper.startsWith('YCL_'))
1397
+ return 'CLAS';
1398
+ if (upper.startsWith('CX_') || upper.startsWith('ZCX_') || upper.startsWith('YCX_'))
1399
+ return 'CLAS';
1400
+ return '';
1401
+ }
1402
+ /**
1403
+ * Map object type + name to the ADT object URL WITHOUT encoding the name.
1404
+ * Used for API release state where the full URI is encoded as a single path segment by the caller.
1405
+ */
1406
+ function objectUrlForTypeRaw(type, name) {
1407
+ return `${objectBasePath(type)}${name}`;
1408
+ }
782
1409
  /** Get the source URL for an object (appends /source/main) */
783
1410
  function sourceUrlForType(type, name) {
784
1411
  return `${objectUrlForType(type, name)}/source/main`;
@@ -796,8 +1423,36 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
796
1423
  }
797
1424
  const objectUrl = objectUrlForType(type, name);
798
1425
  const srcUrl = sourceUrlForType(type, name);
1426
+ // Helper: enforce allowedPackages for existing objects (update/delete/edit_method).
1427
+ // Only fetches metadata when package restrictions are configured — no extra HTTP call otherwise.
1428
+ async function enforcePackageForExistingObject() {
1429
+ if (client.safety.allowedPackages.length === 0)
1430
+ return undefined;
1431
+ const pkg = await client.resolveObjectPackage(objectUrl);
1432
+ if (pkg)
1433
+ checkPackage(client.safety, pkg);
1434
+ return pkg;
1435
+ }
799
1436
  switch (action) {
800
1437
  case 'update': {
1438
+ const existingPackage = await enforcePackageForExistingObject();
1439
+ if (isMetadataWriteType(type)) {
1440
+ // Metadata updates are full-XML-replace — we must fetch existing metadata
1441
+ // and merge with provided fields so omitted fields keep their current values.
1442
+ // Without this, updating just labels would reset dataType/typeKind to defaults.
1443
+ const metadataProps = getMetadataWriteProperties(args);
1444
+ const mergedProps = await mergeMetadataWriteProperties(client, type, name, metadataProps);
1445
+ const description = String(args.description ?? mergedProps._description ?? name);
1446
+ const pkg = String(args.package ?? existingPackage ?? mergedProps._package ?? '$TMP');
1447
+ const body = buildCreateXml(type, name, pkg, description, mergedProps);
1448
+ await safeUpdateObject(client.http, client.safety, objectUrl, body, vendorContentTypeForType(type), transport);
1449
+ cachingLayer?.invalidate(type, name);
1450
+ return textResult(`Successfully updated ${type} ${name}.`);
1451
+ }
1452
+ // CDS pre-write validation: reject unsupported syntax early
1453
+ const cdsGuardUpdate = guardCdsSyntax(type, source, cachedFeatures);
1454
+ if (cdsGuardUpdate)
1455
+ return cdsGuardUpdate;
801
1456
  // Pre-write lint validation
802
1457
  const lintWarnings = runPreWriteLint(source, type, name, config);
803
1458
  if (lintWarnings.blocked)
@@ -811,7 +1466,44 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
811
1466
  const pkg = String(args.package ?? '$TMP');
812
1467
  checkPackage(client.safety, pkg);
813
1468
  const description = String(args.description ?? name);
814
- checkPackage(client.safety, pkg);
1469
+ // Pre-flight: check transport requirements for non-$TMP packages when no transport provided.
1470
+ // SAP requires a transport number for objects in transportable packages.
1471
+ // Instead of letting SAP return a cryptic error, we detect this early and return
1472
+ // an actionable error message guiding the LLM to use SAPTransport first.
1473
+ let effectiveTransport = transport;
1474
+ if (!transport && pkg.toUpperCase() !== '$TMP') {
1475
+ try {
1476
+ const transportInfo = await getTransportInfo(client.http, client.safety, objectUrl, pkg, 'I');
1477
+ if (transportInfo.lockedTransport) {
1478
+ // Object is already locked in a transport — use it automatically
1479
+ effectiveTransport = transportInfo.lockedTransport;
1480
+ }
1481
+ else if (!transportInfo.isLocal && transportInfo.recording) {
1482
+ // Transport IS required but none provided — return guidance
1483
+ const existingList = transportInfo.existingTransports.length > 0
1484
+ ? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
1485
+ .slice(0, 10)
1486
+ .map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
1487
+ .join('\n')}`
1488
+ : '';
1489
+ return errorResult(`Package "${pkg}" requires a transport number for object creation, but none was provided.\n\n` +
1490
+ `To fix this, either:\n` +
1491
+ `1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
1492
+ `2. Use SAPTransport(action="create", description="...") to create a new one\n` +
1493
+ `3. Then retry SAPWrite(action="create", ..., transport="<transport_id>")` +
1494
+ existingList);
1495
+ }
1496
+ // isLocal=true or recording=false → no transport needed, proceed without one
1497
+ }
1498
+ catch {
1499
+ // If transportInfo check fails (older system, permissions, etc.), proceed without it.
1500
+ // SAP will return its own error if a transport is actually needed.
1501
+ }
1502
+ }
1503
+ // CDS pre-write validation: reject unsupported syntax early
1504
+ const cdsGuard = guardCdsSyntax(type, source, cachedFeatures);
1505
+ if (cdsGuard)
1506
+ return cdsGuard;
815
1507
  // AFF header validation (if schema available for this type)
816
1508
  const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
817
1509
  if (!affResult.valid) {
@@ -820,10 +1512,52 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
820
1512
  // Build type-specific creation XML body.
821
1513
  // SAP ADT requires the root element to match the object type —
822
1514
  // a generic objectReferences body returns 400 "System expected the element ...".
823
- const body = buildCreateXml(type, name, pkg, description);
1515
+ const metadataProperties = getMetadataWriteProperties(args);
1516
+ const body = buildCreateXml(type, name, pkg, description, metadataProperties);
824
1517
  // Step 1: Create the object (metadata only)
825
1518
  const createUrl = objectUrl.replace(/\/[^/]+$/, ''); // parent collection URL
826
- const result = await createObject(client.http, client.safety, createUrl, body, 'application/xml', transport);
1519
+ // DOMA/DTEL/BDEF require vendor-specific content types; all other types use
1520
+ // 'application/*' — the wildcard lets the SAP server resolve the correct
1521
+ // handler (matching how ADT Eclipse and abap-adt-api send requests).
1522
+ const contentType = createContentTypeForType(type);
1523
+ const result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport);
1524
+ if (isMetadataWriteType(type)) {
1525
+ // SAP's DTEL POST ignores labels, searchHelp, etc. — they require a follow-up PUT.
1526
+ // Use withStatefulSession directly (not safeUpdateObject) to keep the lock cycle
1527
+ // on the main client's session, avoiding lock contention with subsequent operations.
1528
+ if (type === 'DTEL' && dtelNeedsPostCreateUpdate(metadataProperties)) {
1529
+ const ct = vendorContentTypeForType(type);
1530
+ await client.http.withStatefulSession(async (session) => {
1531
+ const lock = await lockObject(session, client.safety, objectUrl);
1532
+ const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
1533
+ try {
1534
+ await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
1535
+ }
1536
+ finally {
1537
+ await unlockObject(session, objectUrl, lock.lockHandle);
1538
+ }
1539
+ });
1540
+ }
1541
+ // MSAG: POST creates empty container — follow-up PUT to write messages
1542
+ if (type === 'MSAG' && Array.isArray(metadataProperties.messages) && metadataProperties.messages.length > 0) {
1543
+ const ct = vendorContentTypeForType(type);
1544
+ await client.http.withStatefulSession(async (session) => {
1545
+ const lock = await lockObject(session, client.safety, objectUrl);
1546
+ const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
1547
+ try {
1548
+ await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
1549
+ }
1550
+ finally {
1551
+ await unlockObject(session, objectUrl, lock.lockHandle);
1552
+ }
1553
+ });
1554
+ }
1555
+ cachingLayer?.invalidate(type, name);
1556
+ const followUpHint = type === 'SRVB'
1557
+ ? `\n\nNext steps:\n1. SAPActivate(type="SRVB", name="${name}")\n2. SAPActivate(action="publish_srvb", name="${name}")`
1558
+ : '';
1559
+ return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}${followUpHint}`);
1560
+ }
827
1561
  // Step 2: Write source code if provided
828
1562
  if (source) {
829
1563
  // Pre-write lint validation
@@ -831,7 +1565,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
831
1565
  if (lintWarnings.blocked) {
832
1566
  return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
833
1567
  }
834
- await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport);
1568
+ await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport);
835
1569
  cachingLayer?.invalidate(type, name);
836
1570
  const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
837
1571
  return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
@@ -846,6 +1580,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
846
1580
  return errorResult('"source" (new method body) is required for edit_method action.');
847
1581
  if (type !== 'CLAS')
848
1582
  return errorResult('edit_method is only supported for type=CLAS.');
1583
+ await enforcePackageForExistingObject();
849
1584
  // Fetch current full source (use cache if available)
850
1585
  const currentSource = cachingLayer
851
1586
  ? (await cachingLayer.getSource('CLAS', name, () => client.getClass(name))).source
@@ -870,11 +1605,13 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
870
1605
  return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
871
1606
  }
872
1607
  case 'delete': {
873
- // Lock, delete, unlock pattern
1608
+ await enforcePackageForExistingObject();
1609
+ // Lock, delete, unlock pattern — auto-propagate lock corrNr if no explicit transport
874
1610
  await client.http.withStatefulSession(async (session) => {
875
1611
  const lock = await lockObject(session, client.safety, objectUrl);
1612
+ const effectiveTransport = transport ?? (lock.corrNr || undefined);
876
1613
  try {
877
- await deleteObject(session, client.safety, objectUrl, lock.lockHandle, transport);
1614
+ await deleteObject(session, client.safety, objectUrl, lock.lockHandle, effectiveTransport);
878
1615
  }
879
1616
  finally {
880
1617
  try {
@@ -896,10 +1633,41 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
896
1633
  const pkg = String(args.package ?? '$TMP');
897
1634
  // Check package is allowed before starting any creates
898
1635
  checkPackage(client.safety, pkg);
1636
+ // Pre-flight transport check for batch_create (same logic as single create)
1637
+ let batchTransport = transport;
1638
+ if (!transport && pkg.toUpperCase() !== '$TMP') {
1639
+ try {
1640
+ // Use first object's URL for the transport check
1641
+ const firstObj = objects[0];
1642
+ const firstUrl = objectUrlForType(String(firstObj.type ?? ''), String(firstObj.name ?? ''));
1643
+ const transportInfo = await getTransportInfo(client.http, client.safety, firstUrl, pkg, 'I');
1644
+ if (transportInfo.lockedTransport) {
1645
+ batchTransport = transportInfo.lockedTransport;
1646
+ }
1647
+ else if (!transportInfo.isLocal && transportInfo.recording) {
1648
+ const existingList = transportInfo.existingTransports.length > 0
1649
+ ? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
1650
+ .slice(0, 10)
1651
+ .map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
1652
+ .join('\n')}`
1653
+ : '';
1654
+ return errorResult(`Package "${pkg}" requires a transport number for object creation, but none was provided.\n\n` +
1655
+ `To fix this, either:\n` +
1656
+ `1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
1657
+ `2. Use SAPTransport(action="create", description="...") to create a new one\n` +
1658
+ `3. Then retry SAPWrite(action="batch_create", ..., transport="<transport_id>")` +
1659
+ existingList);
1660
+ }
1661
+ }
1662
+ catch {
1663
+ // If transportInfo check fails, proceed — SAP will return its own error if needed.
1664
+ }
1665
+ }
899
1666
  const results = [];
900
1667
  for (const obj of objects) {
901
1668
  const objType = String(obj.type ?? '');
902
1669
  const objName = String(obj.name ?? '');
1670
+ const metadataObject = isMetadataWriteType(objType);
903
1671
  const objSource = obj.source ? String(obj.source) : undefined;
904
1672
  const objDescription = String(obj.description ?? objName);
905
1673
  // AFF header validation per object (if schema available)
@@ -914,8 +1682,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
914
1682
  break;
915
1683
  }
916
1684
  try {
917
- // Pre-validate source with lint BEFORE creating the object to avoid orphaned objects
918
- if (objSource) {
1685
+ // Pre-validate source with lint BEFORE creating the object to avoid orphaned objects.
1686
+ // Metadata objects (DOMA/DTEL) are XML-only and intentionally skip source lint.
1687
+ if (!metadataObject && objSource) {
919
1688
  const lintWarnings = runPreWriteLint(objSource, objType, objName, config);
920
1689
  if (lintWarnings.blocked) {
921
1690
  results.push({
@@ -930,12 +1699,27 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
930
1699
  // Step 1: Create the object
931
1700
  const objUrl = objectUrlForType(objType, objName);
932
1701
  const createUrl = objUrl.replace(/\/[^/]+$/, '');
933
- const body = buildCreateXml(objType, objName, pkg, objDescription);
934
- await createObject(client.http, client.safety, createUrl, body, 'application/xml', transport);
1702
+ const objMetadataProps = getMetadataWriteProperties(obj);
1703
+ const body = buildCreateXml(objType, objName, pkg, objDescription, objMetadataProps);
1704
+ const contentType = createContentTypeForType(objType);
1705
+ await createObject(client.http, client.safety, createUrl, body, contentType, batchTransport);
1706
+ // Step 1b: DTEL POST ignores labels — follow up with PUT on main session
1707
+ if (objType === 'DTEL' && dtelNeedsPostCreateUpdate(objMetadataProps)) {
1708
+ await client.http.withStatefulSession(async (session) => {
1709
+ const lock = await lockObject(session, client.safety, objUrl);
1710
+ const lockTransport = batchTransport ?? (lock.corrNr || undefined);
1711
+ try {
1712
+ await updateObject(session, client.safety, objUrl, body, lock.lockHandle, contentType, lockTransport);
1713
+ }
1714
+ finally {
1715
+ await unlockObject(session, objUrl, lock.lockHandle);
1716
+ }
1717
+ });
1718
+ }
935
1719
  // Step 2: Write source if provided
936
- if (objSource) {
1720
+ if (!metadataObject && objSource) {
937
1721
  const srcUrl = sourceUrlForType(objType, objName);
938
- await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, transport);
1722
+ await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, batchTransport);
939
1723
  }
940
1724
  // Step 3: Activate the object
941
1725
  const activationResult = await activate(client.http, client.safety, objUrl);
@@ -1039,29 +1823,128 @@ function runPreWriteLint(source, type, name, config) {
1039
1823
  }
1040
1824
  // ─── SAPActivate Handler ─────────────────────────────────────────────
1041
1825
  async function handleSAPActivate(client, args) {
1042
- const type = String(args.type ?? '');
1826
+ const action = String(args.action ?? 'activate');
1827
+ const name = String(args.name ?? '');
1828
+ const version = String(args.version ?? '0001');
1829
+ // Publish service binding
1830
+ if (action === 'publish_srvb') {
1831
+ if (!name) {
1832
+ return errorResult('Missing required "name" parameter for publish_srvb action.');
1833
+ }
1834
+ const result = await publishServiceBinding(client.http, client.safety, name, version);
1835
+ if (result.severity === 'ERROR') {
1836
+ return errorResult(`Failed to publish service binding ${name}: ${result.shortText}${result.longText ? ` — ${result.longText}` : ''}`);
1837
+ }
1838
+ let srvbInfo;
1839
+ try {
1840
+ srvbInfo = await client.getSrvb(name);
1841
+ }
1842
+ catch {
1843
+ if (result.severity === 'UNKNOWN') {
1844
+ return errorResult(`Publish response for ${name} could not be parsed and readback failed — use SAPRead to verify publish status.`);
1845
+ }
1846
+ return textResult(`Successfully published service binding ${name} (readback of binding metadata failed — use SAPRead to verify)`);
1847
+ }
1848
+ // Verify the published flag from the SRVB readback
1849
+ try {
1850
+ const srvbData = JSON.parse(srvbInfo);
1851
+ if (srvbData.published === false) {
1852
+ return errorResult(`Publish of service binding ${name} may have failed — binding is still unpublished.\n\n${srvbInfo}`);
1853
+ }
1854
+ }
1855
+ catch {
1856
+ // If we can't parse the readback JSON, fall through — better to return what we have
1857
+ }
1858
+ if (result.severity === 'UNKNOWN') {
1859
+ return textResult(`Publish request for ${name} completed but response could not be fully parsed. Verify status below:\n\n${srvbInfo}`);
1860
+ }
1861
+ return textResult(`Successfully published service binding ${name}.\n\n${srvbInfo}`);
1862
+ }
1863
+ // Unpublish service binding
1864
+ if (action === 'unpublish_srvb') {
1865
+ if (!name) {
1866
+ return errorResult('Missing required "name" parameter for unpublish_srvb action.');
1867
+ }
1868
+ const result = await unpublishServiceBinding(client.http, client.safety, name, version);
1869
+ if (result.severity === 'ERROR') {
1870
+ return errorResult(`Failed to unpublish service binding ${name}: ${result.shortText}${result.longText ? ` — ${result.longText}` : ''}`);
1871
+ }
1872
+ let srvbInfo;
1873
+ try {
1874
+ srvbInfo = await client.getSrvb(name);
1875
+ }
1876
+ catch {
1877
+ // Readback failed — fall through with what we have
1878
+ }
1879
+ // Verify the published flag from the SRVB readback
1880
+ if (srvbInfo) {
1881
+ try {
1882
+ const srvbData = JSON.parse(srvbInfo);
1883
+ if (srvbData.published === true) {
1884
+ return errorResult(`Unpublish of service binding ${name} may have failed — binding is still published.\n\n${srvbInfo}`);
1885
+ }
1886
+ }
1887
+ catch {
1888
+ // If we can't parse the readback JSON, fall through
1889
+ }
1890
+ }
1891
+ if (result.severity === 'UNKNOWN') {
1892
+ return textResult(`Unpublish request for ${name} completed but response could not be fully parsed.${srvbInfo ? ` Verify status below:\n\n${srvbInfo}` : ' Use SAPRead to verify status.'}`);
1893
+ }
1894
+ return textResult(`Successfully unpublished service binding ${name}.${srvbInfo ? `\n\n${srvbInfo}` : ''}`);
1895
+ }
1043
1896
  // Batch activation: multiple objects at once (for RAP stacks etc.)
1897
+ const type = String(args.type ?? '');
1898
+ const preaudit = args.preaudit !== undefined ? Boolean(args.preaudit) : undefined;
1899
+ const activateOpts = preaudit !== undefined ? { preaudit } : undefined;
1044
1900
  if (args.objects && Array.isArray(args.objects)) {
1045
1901
  const objects = args.objects.map((o) => {
1046
1902
  const objType = String(o.type ?? type);
1047
1903
  const objName = String(o.name ?? '');
1048
1904
  return { url: objectUrlForType(objType, objName), name: objName };
1049
1905
  });
1050
- const result = await activateBatch(client.http, client.safety, objects);
1906
+ const result = await activateBatch(client.http, client.safety, objects, activateOpts);
1051
1907
  const names = objects.map((o) => o.name).join(', ');
1052
1908
  if (result.success) {
1053
- return textResult(`Successfully activated ${objects.length} objects: ${names}.${result.messages.length > 0 ? `\nMessages: ${result.messages.join('; ')}` : ''}`);
1909
+ return textResult(`Successfully activated ${objects.length} objects: ${names}.${formatActivationMessages(result)}`);
1054
1910
  }
1055
- return errorResult(`Batch activation failed for: ${names}.\nErrors: ${result.messages.join('; ')}`);
1911
+ return errorResult(`Batch activation failed for: ${names}.\n${formatActivationMessages(result)}`);
1056
1912
  }
1057
1913
  // Single activation (existing behavior)
1058
- const name = String(args.name ?? '');
1059
1914
  const objectUrl = objectUrlForType(type, name);
1060
- const result = await activate(client.http, client.safety, objectUrl);
1915
+ const result = await activate(client.http, client.safety, objectUrl, activateOpts);
1061
1916
  if (result.success) {
1062
- return textResult(`Successfully activated ${type} ${name}.${result.messages.length > 0 ? `\nMessages: ${result.messages.join('; ')}` : ''}`);
1917
+ return textResult(`Successfully activated ${type} ${name}.${formatActivationMessages(result)}`);
1918
+ }
1919
+ return errorResult(`Activation failed for ${type} ${name}.\n${formatActivationMessages(result)}`);
1920
+ }
1921
+ /** Format activation result messages with structured detail (line numbers, URIs) when available */
1922
+ function formatActivationMessages(result) {
1923
+ if (result.details.length === 0)
1924
+ return '';
1925
+ const errors = result.details.filter((d) => d.severity === 'error');
1926
+ const warnings = result.details.filter((d) => d.severity === 'warning');
1927
+ const parts = [];
1928
+ if (errors.length > 0) {
1929
+ const formatted = errors.map((e) => {
1930
+ const prefix = e.line ? `[line ${e.line}] ` : '';
1931
+ const suffix = e.uri ? ` (${e.uri})` : '';
1932
+ return `- ${prefix}${e.text}${suffix}`;
1933
+ });
1934
+ parts.push(`Errors:\n${formatted.join('\n')}`);
1935
+ }
1936
+ if (warnings.length > 0) {
1937
+ const formatted = warnings.map((w) => {
1938
+ const prefix = w.line ? `[line ${w.line}] ` : '';
1939
+ return `- ${prefix}${w.text}`;
1940
+ });
1941
+ parts.push(`Warnings:\n${formatted.join('\n')}`);
1942
+ }
1943
+ // Fall back to flat messages if no errors/warnings but info messages exist
1944
+ if (parts.length === 0 && result.messages.length > 0) {
1945
+ return `\nMessages: ${result.messages.join('; ')}`;
1063
1946
  }
1064
- return errorResult(`Activation failed for ${type} ${name}.\nErrors: ${result.messages.join('; ')}`);
1947
+ return parts.length > 0 ? `\n${parts.join('\n')}` : '';
1065
1948
  }
1066
1949
  // ─── SAPNavigate Handler ─────────────────────────────────────────────
1067
1950
  async function handleSAPNavigate(client, args) {
@@ -1137,8 +2020,63 @@ async function handleSAPNavigate(client, args) {
1137
2020
  const proposals = await getCompletion(client.http, client.safety, uri, line, column, source);
1138
2021
  return textResult(JSON.stringify(proposals, null, 2));
1139
2022
  }
2023
+ case 'hierarchy': {
2024
+ const className = String(args.name ?? '').toUpperCase();
2025
+ if (!className) {
2026
+ return errorResult('Provide name (class name) for hierarchy lookup.');
2027
+ }
2028
+ // Sanitize to prevent SQL injection — class names are alphanumeric + underscore + namespace slash
2029
+ const safeName = className.replace(/[^A-Z0-9_/]/g, '');
2030
+ if (safeName !== className) {
2031
+ return errorResult(`Invalid class name: "${className}". Only alphanumeric characters, underscores, and slashes are allowed.`);
2032
+ }
2033
+ const canFreeSQL = isOperationAllowed(client.safety, OperationType.FreeSQL);
2034
+ const canQuery = isOperationAllowed(client.safety, OperationType.Query);
2035
+ if (!canFreeSQL && !canQuery) {
2036
+ return errorResult('Class hierarchy requires data access permissions. ' +
2037
+ 'Enable free SQL (--block-free-sql=false) or table preview (--block-data=false).');
2038
+ }
2039
+ try {
2040
+ let ownRels;
2041
+ let subRels;
2042
+ if (canFreeSQL) {
2043
+ ownRels = await client.runQuery(`SELECT CLSNAME, REFCLSNAME, RELTYPE FROM SEOMETAREL WHERE CLSNAME = '${safeName}'`, 100);
2044
+ subRels = await client.runQuery(`SELECT CLSNAME FROM SEOMETAREL WHERE REFCLSNAME = '${safeName}' AND RELTYPE = '2'`, 100);
2045
+ }
2046
+ else {
2047
+ // Fall back to named table preview (Query op type)
2048
+ ownRels = await client.getTableContents('SEOMETAREL', 100, `CLSNAME = '${safeName}'`);
2049
+ subRels = await client.getTableContents('SEOMETAREL', 100, `REFCLSNAME = '${safeName}' AND RELTYPE = '2'`);
2050
+ }
2051
+ let superclass = null;
2052
+ const interfaces = [];
2053
+ for (let i = 0; i < ownRels.rows.length; i++) {
2054
+ const row = ownRels.rows[i];
2055
+ const reltype = String(row.RELTYPE ?? '').trim();
2056
+ const refName = String(row.REFCLSNAME ?? '').trim();
2057
+ if (reltype === '2') {
2058
+ superclass = refName;
2059
+ }
2060
+ else if (reltype === '1') {
2061
+ interfaces.push(refName);
2062
+ }
2063
+ }
2064
+ const subclasses = [];
2065
+ for (let i = 0; i < subRels.rows.length; i++) {
2066
+ subclasses.push(String(subRels.rows[i].CLSNAME ?? '').trim());
2067
+ }
2068
+ const result = { className: safeName, superclass, interfaces, subclasses };
2069
+ return textResult(JSON.stringify(result, null, 2));
2070
+ }
2071
+ catch (err) {
2072
+ if (err instanceof AdtApiError && err.statusCode === 404) {
2073
+ return errorResult('Cannot query SEOMETAREL — table may not be accessible on this system.');
2074
+ }
2075
+ throw err;
2076
+ }
2077
+ }
1140
2078
  default:
1141
- return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion`);
2079
+ return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion, hierarchy`);
1142
2080
  }
1143
2081
  }
1144
2082
  // ─── SAPDiagnose Handler ─────────────────────────────────────────────
@@ -1211,8 +2149,9 @@ async function handleSAPTransport(client, args) {
1211
2149
  const action = String(args.action ?? '');
1212
2150
  switch (action) {
1213
2151
  case 'list': {
1214
- const user = args.user;
1215
- const transports = await listTransports(client.http, client.safety, user);
2152
+ const user = args.user || client.username;
2153
+ const status = args.status ?? 'D';
2154
+ const transports = await listTransports(client.http, client.safety, user, status === '*' ? undefined : status);
1216
2155
  return textResult(JSON.stringify(transports, null, 2));
1217
2156
  }
1218
2157
  case 'get': {
@@ -1228,7 +2167,10 @@ async function handleSAPTransport(client, args) {
1228
2167
  const description = String(args.description ?? '');
1229
2168
  if (!description)
1230
2169
  return errorResult('Description is required for "create" action.');
1231
- const id = await createTransport(client.http, client.safety, description);
2170
+ const transportType = String(args.type ?? 'K');
2171
+ const id = await createTransport(client.http, client.safety, description, undefined, transportType);
2172
+ if (!id)
2173
+ return errorResult('Transport creation succeeded but no transport ID was returned. Check the SAP system manually.');
1232
2174
  return textResult(`Created transport request: ${id}`);
1233
2175
  }
1234
2176
  case 'release': {
@@ -1238,8 +2180,61 @@ async function handleSAPTransport(client, args) {
1238
2180
  await releaseTransport(client.http, client.safety, id);
1239
2181
  return textResult(`Released transport request: ${id}`);
1240
2182
  }
2183
+ case 'delete': {
2184
+ const id = String(args.id ?? '');
2185
+ if (!id)
2186
+ return errorResult('Transport ID is required for "delete" action.');
2187
+ const recursive = Boolean(args.recursive ?? false);
2188
+ await deleteTransport(client.http, client.safety, id, recursive);
2189
+ return textResult(`Deleted transport request: ${id}${recursive ? ' (recursive)' : ''}`);
2190
+ }
2191
+ case 'reassign': {
2192
+ const id = String(args.id ?? '');
2193
+ if (!id)
2194
+ return errorResult('Transport ID is required for "reassign" action.');
2195
+ const owner = String(args.owner ?? '');
2196
+ if (!owner)
2197
+ return errorResult('Owner is required for "reassign" action.');
2198
+ const recursive = Boolean(args.recursive ?? false);
2199
+ await reassignTransport(client.http, client.safety, id, owner, recursive);
2200
+ return textResult(`Reassigned transport ${id} to ${owner}${recursive ? ' (recursive)' : ''}`);
2201
+ }
2202
+ case 'release_recursive': {
2203
+ const id = String(args.id ?? '');
2204
+ if (!id)
2205
+ return errorResult('Transport ID is required for "release_recursive" action.');
2206
+ const result = await releaseTransportRecursive(client.http, client.safety, id);
2207
+ return textResult(JSON.stringify(result, null, 2));
2208
+ }
2209
+ case 'check': {
2210
+ // Check transport requirements for an object/package combination.
2211
+ // Does NOT require enableTransports — this is a read-only check.
2212
+ const objectType = String(args.type ?? '');
2213
+ const objectName = String(args.name ?? '');
2214
+ const pkg = String(args.package ?? '');
2215
+ if (!objectType || !objectName)
2216
+ return errorResult('"type" and "name" are required for "check" action.');
2217
+ if (!pkg)
2218
+ return errorResult('"package" is required for "check" action.');
2219
+ const objectUrl = objectUrlForType(objectType, objectName);
2220
+ const info = await getTransportInfo(client.http, client.safety, objectUrl, pkg, 'I');
2221
+ const summary = info.isLocal
2222
+ ? `Package "${pkg}" is local — no transport required.`
2223
+ : info.recording
2224
+ ? `Package "${pkg}" requires a transport for object creation.`
2225
+ : `Package "${pkg}" does not require transport recording.`;
2226
+ return textResult(JSON.stringify({
2227
+ package: pkg,
2228
+ transportRequired: !info.isLocal && info.recording,
2229
+ isLocal: info.isLocal,
2230
+ deliveryUnit: info.deliveryUnit,
2231
+ existingTransports: info.existingTransports,
2232
+ ...(info.lockedTransport ? { lockedTransport: info.lockedTransport } : {}),
2233
+ summary,
2234
+ }, null, 2));
2235
+ }
1241
2236
  default:
1242
- return errorResult(`Unknown SAPTransport action: ${action}. Supported: list, get, create, release`);
2237
+ return errorResult(`Unknown SAPTransport action: ${action}. Supported: list, get, create, release, delete, reassign, release_recursive, check`);
1243
2238
  }
1244
2239
  }
1245
2240
  // ─── SAPContext Handler ───────────────────────────────────────────────
@@ -1354,6 +2349,7 @@ async function handleSAPContext(client, args, cachingLayer) {
1354
2349
  let cachedFeatures;
1355
2350
  async function handleSAPManage(client, config, args, cachingLayer, isPerUserClient) {
1356
2351
  const action = String(args.action ?? '');
2352
+ const flpUnavailableMessage = 'FLP customization service (PAGE_BUILDER_CUST) is not available on this system. Check ICF service activation in SICF.';
1357
2353
  switch (action) {
1358
2354
  case 'features': {
1359
2355
  if (!cachedFeatures) {
@@ -1361,6 +2357,208 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
1361
2357
  }
1362
2358
  return textResult(JSON.stringify(cachedFeatures, null, 2));
1363
2359
  }
2360
+ case 'create_package': {
2361
+ const name = String(args.name ?? '').trim();
2362
+ const description = String(args.description ?? '').trim();
2363
+ const superPackage = String(args.superPackage ?? '').trim();
2364
+ const softwareComponent = String(args.softwareComponent ?? '').trim();
2365
+ const transportLayer = String(args.transportLayer ?? '').trim();
2366
+ const transport = String(args.transport ?? '').trim();
2367
+ if (!name)
2368
+ return errorResult('"name" is required for create_package action.');
2369
+ if (!description)
2370
+ return errorResult('"description" is required for create_package action.');
2371
+ checkOperation(client.safety, OperationType.Create, 'CreatePackage');
2372
+ // Package allowlist is enforced on the parent package, not the new package name.
2373
+ // This enables creating children in allowed parents like $TMP.
2374
+ if (superPackage) {
2375
+ checkPackage(client.safety, superPackage);
2376
+ }
2377
+ let effectiveTransport = transport || undefined;
2378
+ const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
2379
+ // Transport pre-flight for non-local parent packages when no transport is provided.
2380
+ if (!effectiveTransport && superPackage && superPackage.toUpperCase() !== '$TMP') {
2381
+ try {
2382
+ const transportInfo = await getTransportInfo(client.http, client.safety, packageUrl, superPackage, 'I');
2383
+ if (transportInfo.lockedTransport) {
2384
+ effectiveTransport = transportInfo.lockedTransport;
2385
+ }
2386
+ else if (!transportInfo.isLocal && transportInfo.recording) {
2387
+ const existingList = transportInfo.existingTransports.length > 0
2388
+ ? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
2389
+ .slice(0, 10)
2390
+ .map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
2391
+ .join('\n')}`
2392
+ : '';
2393
+ return errorResult(`Package "${superPackage}" requires a transport number for package creation, but none was provided.\n\n` +
2394
+ `To fix this, either:\n` +
2395
+ `1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
2396
+ `2. Use SAPTransport(action="create", description="...") to create a new one\n` +
2397
+ `3. Then retry SAPManage(action="create_package", ..., transport="<transport_id>")` +
2398
+ existingList);
2399
+ }
2400
+ }
2401
+ catch {
2402
+ // Graceful fallback: let SAP enforce transport requirements if the pre-check fails.
2403
+ }
2404
+ }
2405
+ const packageTypeRaw = String(args.packageType ?? '').trim();
2406
+ const packageType = packageTypeRaw === 'development' || packageTypeRaw === 'structure' || packageTypeRaw === 'main'
2407
+ ? packageTypeRaw
2408
+ : undefined;
2409
+ const xml = buildPackageXml({
2410
+ name,
2411
+ description,
2412
+ superPackage: superPackage || undefined,
2413
+ softwareComponent: softwareComponent || undefined,
2414
+ transportLayer: transportLayer || undefined,
2415
+ packageType,
2416
+ });
2417
+ await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport);
2418
+ return textResult(`Created package ${name}.`);
2419
+ }
2420
+ case 'delete_package': {
2421
+ const name = String(args.name ?? '').trim();
2422
+ const transport = String(args.transport ?? '').trim();
2423
+ if (!name)
2424
+ return errorResult('"name" is required for delete_package action.');
2425
+ checkOperation(client.safety, OperationType.Delete, 'DeletePackage');
2426
+ const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
2427
+ await client.http.withStatefulSession(async (session) => {
2428
+ const lock = await lockObject(session, client.safety, packageUrl);
2429
+ const effectiveTransport = transport || lock.corrNr || undefined;
2430
+ try {
2431
+ await deleteObject(session, client.safety, packageUrl, lock.lockHandle, effectiveTransport);
2432
+ }
2433
+ finally {
2434
+ try {
2435
+ await unlockObject(session, packageUrl, lock.lockHandle);
2436
+ }
2437
+ catch {
2438
+ // Object may already be deleted — unlock failure is expected.
2439
+ }
2440
+ }
2441
+ });
2442
+ return textResult(`Deleted package ${name}.`);
2443
+ }
2444
+ case 'flp_list_catalogs': {
2445
+ const catalogs = await listCatalogs(client.http, client.safety);
2446
+ const customCount = catalogs.filter((c) => /^(Z|Y)/i.test(c.domainId)).length;
2447
+ const lines = [
2448
+ `${catalogs.length} catalogs (${customCount} custom Z/Y). Columns: domainId | title | type | scope | chips`,
2449
+ ...catalogs.map((c) => `${c.domainId} | ${c.title || '(no title)'} | ${c.type || '-'} | ${c.scope || '-'} | ${c.chipCount}`),
2450
+ ];
2451
+ return textResult(lines.join('\n'));
2452
+ }
2453
+ case 'flp_list_groups': {
2454
+ const groups = await listGroups(client.http, client.safety);
2455
+ const lines = [
2456
+ `${groups.length} groups. Columns: id | title`,
2457
+ ...groups.map((g) => `${g.id} | ${g.title || '(no title)'}`),
2458
+ ];
2459
+ return textResult(lines.join('\n'));
2460
+ }
2461
+ case 'flp_list_tiles': {
2462
+ const catalogId = String(args.catalogId ?? '');
2463
+ if (!catalogId)
2464
+ return errorResult('"catalogId" is required for flp_list_tiles action.');
2465
+ const result = await listTiles(client.http, client.safety, catalogId);
2466
+ if (result.backendError) {
2467
+ return textResult(`⚠ Backend error for catalog "${catalogId}": ${result.backendError}\n\nReturned 0 tiles.`);
2468
+ }
2469
+ const lines = [
2470
+ `${result.tiles.length} tiles in catalog "${catalogId}". Columns: instanceId | title | chipId | semanticObject | semanticAction`,
2471
+ ...result.tiles.map((t) => {
2472
+ const so = t.configuration?.semantic_object ?? '';
2473
+ const sa = t.configuration?.semantic_action ?? '';
2474
+ return `${t.instanceId} | ${t.title || '(no title)'} | ${t.chipId} | ${so} | ${sa}`;
2475
+ }),
2476
+ ];
2477
+ return textResult(lines.join('\n'));
2478
+ }
2479
+ case 'flp_create_catalog': {
2480
+ if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
2481
+ return errorResult(flpUnavailableMessage);
2482
+ }
2483
+ const domainId = String(args.domainId ?? '');
2484
+ const title = String(args.title ?? '');
2485
+ if (!domainId)
2486
+ return errorResult('"domainId" is required for flp_create_catalog action.');
2487
+ if (!title)
2488
+ return errorResult('"title" is required for flp_create_catalog action.');
2489
+ const catalog = await createCatalog(client.http, client.safety, domainId, title);
2490
+ return textResult(JSON.stringify(catalog, null, 2));
2491
+ }
2492
+ case 'flp_create_group': {
2493
+ if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
2494
+ return errorResult(flpUnavailableMessage);
2495
+ }
2496
+ const groupId = String(args.groupId ?? '');
2497
+ const title = String(args.title ?? '');
2498
+ if (!groupId)
2499
+ return errorResult('"groupId" is required for flp_create_group action.');
2500
+ if (!title)
2501
+ return errorResult('"title" is required for flp_create_group action.');
2502
+ const group = await createGroup(client.http, client.safety, groupId, title);
2503
+ return textResult(JSON.stringify(group, null, 2));
2504
+ }
2505
+ case 'flp_create_tile': {
2506
+ if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
2507
+ return errorResult(flpUnavailableMessage);
2508
+ }
2509
+ const catalogId = String(args.catalogId ?? '');
2510
+ if (!catalogId)
2511
+ return errorResult('"catalogId" is required for flp_create_tile action.');
2512
+ const rawTile = args.tile;
2513
+ if (!rawTile || typeof rawTile !== 'object' || Array.isArray(rawTile)) {
2514
+ return errorResult('"tile" object is required for flp_create_tile action.');
2515
+ }
2516
+ const tile = rawTile;
2517
+ const id = String(tile.id ?? '');
2518
+ const title = String(tile.title ?? '');
2519
+ const semanticObject = String(tile.semanticObject ?? '');
2520
+ const semanticAction = String(tile.semanticAction ?? '');
2521
+ if (!id || !title || !semanticObject || !semanticAction) {
2522
+ return errorResult('"tile.id", "tile.title", "tile.semanticObject", and "tile.semanticAction" are required for flp_create_tile action.');
2523
+ }
2524
+ const tileInstance = await createTile(client.http, client.safety, catalogId, {
2525
+ id,
2526
+ title,
2527
+ semanticObject,
2528
+ semanticAction,
2529
+ icon: typeof tile.icon === 'string' ? tile.icon : undefined,
2530
+ url: typeof tile.url === 'string' ? tile.url : undefined,
2531
+ subtitle: typeof tile.subtitle === 'string' ? tile.subtitle : undefined,
2532
+ info: typeof tile.info === 'string' ? tile.info : undefined,
2533
+ });
2534
+ return textResult(JSON.stringify(tileInstance, null, 2));
2535
+ }
2536
+ case 'flp_add_tile_to_group': {
2537
+ if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
2538
+ return errorResult(flpUnavailableMessage);
2539
+ }
2540
+ const groupId = String(args.groupId ?? '');
2541
+ const catalogId = String(args.catalogId ?? '');
2542
+ const tileInstanceId = String(args.tileInstanceId ?? '');
2543
+ if (!groupId)
2544
+ return errorResult('"groupId" is required for flp_add_tile_to_group action.');
2545
+ if (!catalogId)
2546
+ return errorResult('"catalogId" is required for flp_add_tile_to_group action.');
2547
+ if (!tileInstanceId)
2548
+ return errorResult('"tileInstanceId" is required for flp_add_tile_to_group action.');
2549
+ const result = await addTileToGroup(client.http, client.safety, groupId, catalogId, tileInstanceId);
2550
+ return textResult(JSON.stringify(result, null, 2));
2551
+ }
2552
+ case 'flp_delete_catalog': {
2553
+ if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
2554
+ return errorResult(flpUnavailableMessage);
2555
+ }
2556
+ const catalogId = String(args.catalogId ?? '');
2557
+ if (!catalogId)
2558
+ return errorResult('"catalogId" is required for flp_delete_catalog action.');
2559
+ await deleteCatalog(client.http, client.safety, catalogId);
2560
+ return textResult(`Deleted FLP catalog: ${catalogId}`);
2561
+ }
1364
2562
  case 'cache_stats': {
1365
2563
  if (!cachingLayer) {
1366
2564
  return textResult(JSON.stringify({ enabled: false, message: 'Object cache is disabled (ARC1_CACHE=none).' }));
@@ -1382,6 +2580,8 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
1382
2580
  featureConfig.amdp = config.featureAmdp;
1383
2581
  featureConfig.ui5 = config.featureUi5;
1384
2582
  featureConfig.transport = config.featureTransport;
2583
+ featureConfig.ui5repo = config.featureUi5Repo;
2584
+ featureConfig.flp = config.featureFlp;
1385
2585
  const probed = await probeFeatures(client.http, featureConfig, config.systemType);
1386
2586
  // In PP mode with a per-user client, auth-sensitive results (401/403 on any
1387
2587
  // feature) must not poison the global cache — another user may have different
@@ -1405,7 +2605,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
1405
2605
  return textResult(JSON.stringify(probed, null, 2));
1406
2606
  }
1407
2607
  default:
1408
- return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe, cache_stats`);
2608
+ return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe, cache_stats, create_package, delete_package, flp_list_catalogs, flp_list_groups, flp_list_tiles, flp_create_catalog, flp_create_group, flp_create_tile, flp_add_tile_to_group, flp_delete_catalog`);
1409
2609
  }
1410
2610
  }
1411
2611
  /** Reset cached features (for testing) */