arc-1 0.6.1 → 0.6.3

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 (83) hide show
  1. package/README.md +18 -20
  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 +12 -2
  7. package/dist/adt/client.d.ts.map +1 -1
  8. package/dist/adt/client.js +27 -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 +1 -0
  14. package/dist/adt/config.d.ts.map +1 -1
  15. package/dist/adt/config.js +1 -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 +33 -2
  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 +19 -11
  26. package/dist/adt/devtools.d.ts.map +1 -1
  27. package/dist/adt/devtools.js +61 -17
  28. package/dist/adt/devtools.js.map +1 -1
  29. package/dist/adt/errors.d.ts +12 -0
  30. package/dist/adt/errors.d.ts.map +1 -1
  31. package/dist/adt/errors.js +42 -0
  32. package/dist/adt/errors.js.map +1 -1
  33. package/dist/adt/features.d.ts.map +1 -1
  34. package/dist/adt/features.js +20 -3
  35. package/dist/adt/features.js.map +1 -1
  36. package/dist/adt/flp.d.ts +43 -0
  37. package/dist/adt/flp.d.ts.map +1 -0
  38. package/dist/adt/flp.js +213 -0
  39. package/dist/adt/flp.js.map +1 -0
  40. package/dist/adt/http.d.ts +2 -0
  41. package/dist/adt/http.d.ts.map +1 -1
  42. package/dist/adt/http.js +83 -6
  43. package/dist/adt/http.js.map +1 -1
  44. package/dist/adt/transport.d.ts +42 -3
  45. package/dist/adt/transport.d.ts.map +1 -1
  46. package/dist/adt/transport.js +197 -20
  47. package/dist/adt/transport.js.map +1 -1
  48. package/dist/adt/types.d.ts +58 -0
  49. package/dist/adt/types.d.ts.map +1 -1
  50. package/dist/adt/xml-parser.d.ts +14 -1
  51. package/dist/adt/xml-parser.d.ts.map +1 -1
  52. package/dist/adt/xml-parser.js +70 -1
  53. package/dist/adt/xml-parser.js.map +1 -1
  54. package/dist/handlers/intent.d.ts +4 -6
  55. package/dist/handlers/intent.d.ts.map +1 -1
  56. package/dist/handlers/intent.js +931 -47
  57. package/dist/handlers/intent.js.map +1 -1
  58. package/dist/handlers/schemas.d.ts +225 -0
  59. package/dist/handlers/schemas.d.ts.map +1 -1
  60. package/dist/handlers/schemas.js +190 -4
  61. package/dist/handlers/schemas.js.map +1 -1
  62. package/dist/handlers/tools.d.ts.map +1 -1
  63. package/dist/handlers/tools.js +308 -43
  64. package/dist/handlers/tools.js.map +1 -1
  65. package/dist/server/config.d.ts.map +1 -1
  66. package/dist/server/config.js +14 -3
  67. package/dist/server/config.js.map +1 -1
  68. package/dist/server/http.d.ts.map +1 -1
  69. package/dist/server/http.js +32 -3
  70. package/dist/server/http.js.map +1 -1
  71. package/dist/server/server.d.ts +1 -1
  72. package/dist/server/server.d.ts.map +1 -1
  73. package/dist/server/server.js +37 -6
  74. package/dist/server/server.js.map +1 -1
  75. package/dist/server/types.d.ts +1 -0
  76. package/dist/server/types.d.ts.map +1 -1
  77. package/dist/server/types.js +4 -3
  78. package/dist/server/types.js.map +1 -1
  79. package/dist/server/xsuaa.d.ts +13 -0
  80. package/dist/server/xsuaa.d.ts.map +1 -1
  81. package/dist/server/xsuaa.js +28 -2
  82. package/dist/server/xsuaa.js.map +1 -1
  83. package/package.json +10 -5
@@ -10,13 +10,15 @@
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';
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';
14
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';
20
22
  import { getAppInfo } from '../adt/ui5-repository.js';
21
23
  import { validateAffHeader } from '../aff/validator.js';
22
24
  import { extractCdsElements } from '../context/cds-deps.js';
@@ -115,34 +117,65 @@ export function looksLikeFieldName(query) {
115
117
  return false;
116
118
  return true;
117
119
  }
118
- /** Classify error type for audit logging */
119
120
  /** Format error messages with LLM-friendly remediation hints */
120
121
  function formatErrorForLLM(err, message, _tool, args) {
121
122
  if (err instanceof AdtApiError) {
123
+ // Append additional SAP messages (line numbers, secondary errors) if available
124
+ const enriched = enrichWithSapDetails(err, message);
122
125
  if (err.isNotFound) {
123
126
  const name = String(args.name ?? '');
124
127
  const type = String(args.type ?? '');
125
- 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.`;
126
129
  }
127
130
  if (err.isUnauthorized || err.isForbidden) {
128
- 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.`;
129
132
  }
130
133
  // Transport / corrNr specific hints
131
134
  const transportHint = getTransportHint(err);
132
135
  if (transportHint) {
133
- return `${message}\n\nHint: ${transportHint}`;
136
+ return `${enriched}\n\nHint: ${transportHint}`;
134
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").`;
141
+ }
142
+ return enriched;
135
143
  }
136
144
  if (err instanceof AdtNetworkError) {
137
145
  return `${message}\n\nHint: Cannot reach the SAP system. This is a connectivity issue, not a usage error.`;
138
146
  }
139
147
  return message;
140
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
+ }
141
171
  /** Detect transport/corrNr failure signatures and return a remediation hint, or undefined if not transport-related. */
142
172
  function getTransportHint(err) {
143
173
  const body = (err.responseBody ?? '').toLowerCase();
144
- const msg = err.message.toLowerCase();
145
- const combined = `${msg} ${body}`;
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}`;
146
179
  // Missing or invalid transport/correction number
147
180
  if (combined.includes('correction number') ||
148
181
  combined.includes('corrnr') ||
@@ -468,6 +501,10 @@ async function handleSAPRead(client, args, cachingLayer) {
468
501
  }
469
502
  case 'DDLS': {
470
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
+ }
471
508
  if (args.include?.toLowerCase() === 'elements') {
472
509
  // Elements extraction is derived from source — no cache indicator
473
510
  return textResult(extractCdsElements(ddlSource, name));
@@ -590,8 +627,16 @@ async function handleSAPRead(client, args, cachingLayer) {
590
627
  const components = await client.getInstalledComponents();
591
628
  return textResult(JSON.stringify(components, null, 2));
592
629
  }
593
- case 'MESSAGES':
594
- 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
+ }
595
640
  case 'TEXT_ELEMENTS':
596
641
  return textResult(await client.getTextElements(name));
597
642
  case 'VARIANTS':
@@ -631,8 +676,22 @@ async function handleSAPRead(client, args, cachingLayer) {
631
676
  }
632
677
  return textResult(JSON.stringify(info, null, 2));
633
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
+ }
634
693
  default:
635
- 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. ` +
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. ` +
636
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"). ' +
637
696
  'Do not pass a URI — use the "type" and "name" parameters instead.');
638
697
  }
@@ -784,6 +843,187 @@ function buildLintConfigOptions(config, ruleOverrides) {
784
843
  };
785
844
  }
786
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
+ }
787
1027
  /**
788
1028
  * Build the type-specific XML body for ADT object creation.
789
1029
  *
@@ -791,7 +1031,101 @@ function buildLintConfigOptions(config, ruleOverrides) {
791
1031
  * Using a generic body (e.g. adtcore:objectReferences) returns 400:
792
1032
  * "System expected the element '{http://www.sap.com/adt/programs/programs}abapProgram'"
793
1033
  */
794
- 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) {
795
1129
  switch (type) {
796
1130
  case 'PROG':
797
1131
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -850,45 +1184,145 @@ export function buildCreateXml(type, name, pkg, description) {
850
1184
  adtcore:type="DDLS/DF"
851
1185
  adtcore:masterLanguage="EN"
852
1186
  adtcore:masterSystem="H00"
853
- adtcore:responsible="DEVELOPER">
1187
+ adtcore:responsible="DEVELOPER">
854
1188
  <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
855
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>`;
856
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.
857
1206
  return `<?xml version="1.0" encoding="UTF-8"?>
858
- <bdef:behaviorDefinition xmlns:bdef="http://www.sap.com/adt/bo/behaviordefinitions"
859
- xmlns:adtcore="http://www.sap.com/adt/core"
860
- adtcore:description="${escapeXml(description)}"
861
- adtcore:name="${escapeXml(name)}"
862
- adtcore:type="BDEF/BDO"
863
- adtcore:masterLanguage="EN"
864
- adtcore:masterSystem="H00"
865
- 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">
866
1215
  <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
867
- </bdef:behaviorDefinition>`;
1216
+ </blue:blueSource>`;
868
1217
  case 'SRVD':
869
1218
  return `<?xml version="1.0" encoding="UTF-8"?>
870
- <srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/srvd/sources"
1219
+ <srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/srvdsources"
871
1220
  xmlns:adtcore="http://www.sap.com/adt/core"
872
1221
  adtcore:description="${escapeXml(description)}"
873
1222
  adtcore:name="${escapeXml(name)}"
874
1223
  adtcore:type="SRVD/SRV"
875
1224
  adtcore:masterLanguage="EN"
876
1225
  adtcore:masterSystem="H00"
877
- adtcore:responsible="DEVELOPER">
1226
+ adtcore:responsible="DEVELOPER"
1227
+ srvd:srvdSourceType="S">
878
1228
  <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
879
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
+ }
880
1248
  case 'DDLX':
881
1249
  return `<?xml version="1.0" encoding="UTF-8"?>
882
- <ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/ddlx/sources"
1250
+ <ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/ddlxsources"
883
1251
  xmlns:adtcore="http://www.sap.com/adt/core"
884
1252
  adtcore:description="${escapeXml(description)}"
885
1253
  adtcore:name="${escapeXml(name)}"
886
1254
  adtcore:type="DDLX/EX"
887
1255
  adtcore:masterLanguage="EN"
888
1256
  adtcore:masterSystem="H00"
889
- adtcore:responsible="DEVELOPER">
1257
+ adtcore:responsible="DEVELOPER">
890
1258
  <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
891
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
+ }
892
1326
  default:
893
1327
  // Fallback — generic objectReferences using the correct URL for the type
894
1328
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -940,6 +1374,10 @@ function objectBasePath(type) {
940
1374
  return '/sap/bc/adt/ddic/domains/';
941
1375
  case 'DTEL':
942
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/';
943
1381
  case 'TRAN':
944
1382
  return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
945
1383
  default:
@@ -985,8 +1423,36 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
985
1423
  }
986
1424
  const objectUrl = objectUrlForType(type, name);
987
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
+ }
988
1436
  switch (action) {
989
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;
990
1456
  // Pre-write lint validation
991
1457
  const lintWarnings = runPreWriteLint(source, type, name, config);
992
1458
  if (lintWarnings.blocked)
@@ -1000,6 +1466,44 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1000
1466
  const pkg = String(args.package ?? '$TMP');
1001
1467
  checkPackage(client.safety, pkg);
1002
1468
  const description = String(args.description ?? name);
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;
1003
1507
  // AFF header validation (if schema available for this type)
1004
1508
  const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
1005
1509
  if (!affResult.valid) {
@@ -1008,10 +1512,52 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1008
1512
  // Build type-specific creation XML body.
1009
1513
  // SAP ADT requires the root element to match the object type —
1010
1514
  // a generic objectReferences body returns 400 "System expected the element ...".
1011
- const body = buildCreateXml(type, name, pkg, description);
1515
+ const metadataProperties = getMetadataWriteProperties(args);
1516
+ const body = buildCreateXml(type, name, pkg, description, metadataProperties);
1012
1517
  // Step 1: Create the object (metadata only)
1013
1518
  const createUrl = objectUrl.replace(/\/[^/]+$/, ''); // parent collection URL
1014
- 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
+ }
1015
1561
  // Step 2: Write source code if provided
1016
1562
  if (source) {
1017
1563
  // Pre-write lint validation
@@ -1019,7 +1565,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1019
1565
  if (lintWarnings.blocked) {
1020
1566
  return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
1021
1567
  }
1022
- await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport);
1568
+ await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport);
1023
1569
  cachingLayer?.invalidate(type, name);
1024
1570
  const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
1025
1571
  return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
@@ -1034,6 +1580,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1034
1580
  return errorResult('"source" (new method body) is required for edit_method action.');
1035
1581
  if (type !== 'CLAS')
1036
1582
  return errorResult('edit_method is only supported for type=CLAS.');
1583
+ await enforcePackageForExistingObject();
1037
1584
  // Fetch current full source (use cache if available)
1038
1585
  const currentSource = cachingLayer
1039
1586
  ? (await cachingLayer.getSource('CLAS', name, () => client.getClass(name))).source
@@ -1058,6 +1605,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1058
1605
  return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
1059
1606
  }
1060
1607
  case 'delete': {
1608
+ await enforcePackageForExistingObject();
1061
1609
  // Lock, delete, unlock pattern — auto-propagate lock corrNr if no explicit transport
1062
1610
  await client.http.withStatefulSession(async (session) => {
1063
1611
  const lock = await lockObject(session, client.safety, objectUrl);
@@ -1085,10 +1633,41 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1085
1633
  const pkg = String(args.package ?? '$TMP');
1086
1634
  // Check package is allowed before starting any creates
1087
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
+ }
1088
1666
  const results = [];
1089
1667
  for (const obj of objects) {
1090
1668
  const objType = String(obj.type ?? '');
1091
1669
  const objName = String(obj.name ?? '');
1670
+ const metadataObject = isMetadataWriteType(objType);
1092
1671
  const objSource = obj.source ? String(obj.source) : undefined;
1093
1672
  const objDescription = String(obj.description ?? objName);
1094
1673
  // AFF header validation per object (if schema available)
@@ -1103,8 +1682,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1103
1682
  break;
1104
1683
  }
1105
1684
  try {
1106
- // Pre-validate source with lint BEFORE creating the object to avoid orphaned objects
1107
- 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) {
1108
1688
  const lintWarnings = runPreWriteLint(objSource, objType, objName, config);
1109
1689
  if (lintWarnings.blocked) {
1110
1690
  results.push({
@@ -1119,12 +1699,27 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1119
1699
  // Step 1: Create the object
1120
1700
  const objUrl = objectUrlForType(objType, objName);
1121
1701
  const createUrl = objUrl.replace(/\/[^/]+$/, '');
1122
- const body = buildCreateXml(objType, objName, pkg, objDescription);
1123
- 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
+ }
1124
1719
  // Step 2: Write source if provided
1125
- if (objSource) {
1720
+ if (!metadataObject && objSource) {
1126
1721
  const srcUrl = sourceUrlForType(objType, objName);
1127
- await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, transport);
1722
+ await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, batchTransport);
1128
1723
  }
1129
1724
  // Step 3: Activate the object
1130
1725
  const activationResult = await activate(client.http, client.safety, objUrl);
@@ -1300,26 +1895,56 @@ async function handleSAPActivate(client, args) {
1300
1895
  }
1301
1896
  // Batch activation: multiple objects at once (for RAP stacks etc.)
1302
1897
  const type = String(args.type ?? '');
1898
+ const preaudit = args.preaudit !== undefined ? Boolean(args.preaudit) : undefined;
1899
+ const activateOpts = preaudit !== undefined ? { preaudit } : undefined;
1303
1900
  if (args.objects && Array.isArray(args.objects)) {
1304
1901
  const objects = args.objects.map((o) => {
1305
1902
  const objType = String(o.type ?? type);
1306
1903
  const objName = String(o.name ?? '');
1307
1904
  return { url: objectUrlForType(objType, objName), name: objName };
1308
1905
  });
1309
- const result = await activateBatch(client.http, client.safety, objects);
1906
+ const result = await activateBatch(client.http, client.safety, objects, activateOpts);
1310
1907
  const names = objects.map((o) => o.name).join(', ');
1311
1908
  if (result.success) {
1312
- 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)}`);
1313
1910
  }
1314
- return errorResult(`Batch activation failed for: ${names}.\nErrors: ${result.messages.join('; ')}`);
1911
+ return errorResult(`Batch activation failed for: ${names}.\n${formatActivationMessages(result)}`);
1315
1912
  }
1316
1913
  // Single activation (existing behavior)
1317
1914
  const objectUrl = objectUrlForType(type, name);
1318
- const result = await activate(client.http, client.safety, objectUrl);
1915
+ const result = await activate(client.http, client.safety, objectUrl, activateOpts);
1319
1916
  if (result.success) {
1320
- return textResult(`Successfully activated ${type} ${name}.${result.messages.length > 0 ? `\nMessages: ${result.messages.join('; ')}` : ''}`);
1917
+ return textResult(`Successfully activated ${type} ${name}.${formatActivationMessages(result)}`);
1321
1918
  }
1322
- return errorResult(`Activation failed for ${type} ${name}.\nErrors: ${result.messages.join('; ')}`);
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('; ')}`;
1946
+ }
1947
+ return parts.length > 0 ? `\n${parts.join('\n')}` : '';
1323
1948
  }
1324
1949
  // ─── SAPNavigate Handler ─────────────────────────────────────────────
1325
1950
  async function handleSAPNavigate(client, args) {
@@ -1524,8 +2149,9 @@ async function handleSAPTransport(client, args) {
1524
2149
  const action = String(args.action ?? '');
1525
2150
  switch (action) {
1526
2151
  case 'list': {
1527
- const user = args.user;
1528
- 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);
1529
2155
  return textResult(JSON.stringify(transports, null, 2));
1530
2156
  }
1531
2157
  case 'get': {
@@ -1541,7 +2167,8 @@ async function handleSAPTransport(client, args) {
1541
2167
  const description = String(args.description ?? '');
1542
2168
  if (!description)
1543
2169
  return errorResult('Description is required for "create" action.');
1544
- 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);
1545
2172
  if (!id)
1546
2173
  return errorResult('Transport creation succeeded but no transport ID was returned. Check the SAP system manually.');
1547
2174
  return textResult(`Created transport request: ${id}`);
@@ -1553,8 +2180,61 @@ async function handleSAPTransport(client, args) {
1553
2180
  await releaseTransport(client.http, client.safety, id);
1554
2181
  return textResult(`Released transport request: ${id}`);
1555
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
+ }
1556
2236
  default:
1557
- 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`);
1558
2238
  }
1559
2239
  }
1560
2240
  // ─── SAPContext Handler ───────────────────────────────────────────────
@@ -1669,6 +2349,7 @@ async function handleSAPContext(client, args, cachingLayer) {
1669
2349
  let cachedFeatures;
1670
2350
  async function handleSAPManage(client, config, args, cachingLayer, isPerUserClient) {
1671
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.';
1672
2353
  switch (action) {
1673
2354
  case 'features': {
1674
2355
  if (!cachedFeatures) {
@@ -1676,6 +2357,208 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
1676
2357
  }
1677
2358
  return textResult(JSON.stringify(cachedFeatures, null, 2));
1678
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
+ }
1679
2562
  case 'cache_stats': {
1680
2563
  if (!cachingLayer) {
1681
2564
  return textResult(JSON.stringify({ enabled: false, message: 'Object cache is disabled (ARC1_CACHE=none).' }));
@@ -1698,6 +2581,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
1698
2581
  featureConfig.ui5 = config.featureUi5;
1699
2582
  featureConfig.transport = config.featureTransport;
1700
2583
  featureConfig.ui5repo = config.featureUi5Repo;
2584
+ featureConfig.flp = config.featureFlp;
1701
2585
  const probed = await probeFeatures(client.http, featureConfig, config.systemType);
1702
2586
  // In PP mode with a per-user client, auth-sensitive results (401/403 on any
1703
2587
  // feature) must not poison the global cache — another user may have different
@@ -1721,7 +2605,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
1721
2605
  return textResult(JSON.stringify(probed, null, 2));
1722
2606
  }
1723
2607
  default:
1724
- 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`);
1725
2609
  }
1726
2610
  }
1727
2611
  /** Reset cached features (for testing) */