arc-1 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +10 -9
  2. package/dist/adt/client.d.ts +9 -1
  3. package/dist/adt/client.d.ts.map +1 -1
  4. package/dist/adt/client.js +39 -1
  5. package/dist/adt/client.js.map +1 -1
  6. package/dist/adt/config.d.ts +1 -0
  7. package/dist/adt/config.d.ts.map +1 -1
  8. package/dist/adt/config.js +1 -0
  9. package/dist/adt/config.js.map +1 -1
  10. package/dist/adt/crud.d.ts.map +1 -1
  11. package/dist/adt/crud.js +2 -1
  12. package/dist/adt/crud.js.map +1 -1
  13. package/dist/adt/devtools.d.ts +15 -0
  14. package/dist/adt/devtools.d.ts.map +1 -1
  15. package/dist/adt/devtools.js +162 -67
  16. package/dist/adt/devtools.js.map +1 -1
  17. package/dist/adt/diagnostics.d.ts.map +1 -1
  18. package/dist/adt/diagnostics.js +105 -184
  19. package/dist/adt/diagnostics.js.map +1 -1
  20. package/dist/adt/features.d.ts.map +1 -1
  21. package/dist/adt/features.js +3 -0
  22. package/dist/adt/features.js.map +1 -1
  23. package/dist/adt/http.d.ts.map +1 -1
  24. package/dist/adt/http.js +83 -2
  25. package/dist/adt/http.js.map +1 -1
  26. package/dist/adt/transport.d.ts +6 -0
  27. package/dist/adt/transport.d.ts.map +1 -1
  28. package/dist/adt/transport.js +35 -22
  29. package/dist/adt/transport.js.map +1 -1
  30. package/dist/adt/types.d.ts +49 -0
  31. package/dist/adt/types.d.ts.map +1 -1
  32. package/dist/adt/ui5-repository.d.ts +19 -0
  33. package/dist/adt/ui5-repository.d.ts.map +1 -0
  34. package/dist/adt/ui5-repository.js +43 -0
  35. package/dist/adt/ui5-repository.js.map +1 -0
  36. package/dist/adt/xml-parser.d.ts +30 -1
  37. package/dist/adt/xml-parser.d.ts.map +1 -1
  38. package/dist/adt/xml-parser.js +138 -2
  39. package/dist/adt/xml-parser.js.map +1 -1
  40. package/dist/handlers/intent.d.ts +16 -0
  41. package/dist/handlers/intent.d.ts.map +1 -1
  42. package/dist/handlers/intent.js +377 -61
  43. package/dist/handlers/intent.js.map +1 -1
  44. package/dist/handlers/schemas.d.ts +15 -0
  45. package/dist/handlers/schemas.d.ts.map +1 -1
  46. package/dist/handlers/schemas.js +11 -1
  47. package/dist/handlers/schemas.js.map +1 -1
  48. package/dist/handlers/tools.d.ts.map +1 -1
  49. package/dist/handlers/tools.js +34 -9
  50. package/dist/handlers/tools.js.map +1 -1
  51. package/dist/server/config.d.ts.map +1 -1
  52. package/dist/server/config.js +1 -0
  53. package/dist/server/config.js.map +1 -1
  54. package/dist/server/server.d.ts +1 -1
  55. package/dist/server/server.d.ts.map +1 -1
  56. package/dist/server/server.js +2 -1
  57. package/dist/server/server.js.map +1 -1
  58. package/dist/server/types.d.ts +1 -0
  59. package/dist/server/types.d.ts.map +1 -1
  60. package/dist/server/types.js +1 -0
  61. package/dist/server/types.js.map +1 -1
  62. package/package.json +12 -3
@@ -11,12 +11,13 @@
11
11
  */
12
12
  import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '../adt/codeintel.js';
13
13
  import { createObject, deleteObject, lockObject, safeUpdateSource, unlockObject } from '../adt/crud.js';
14
- import { activate, activateBatch, runAtcCheck, runUnitTests, syntaxCheck } from '../adt/devtools.js';
14
+ import { activate, activateBatch, publishServiceBinding, runAtcCheck, runUnitTests, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
15
15
  import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
16
16
  import { AdtApiError, AdtNetworkError, AdtSafetyError, isNotFoundError } from '../adt/errors.js';
17
17
  import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
18
18
  import { checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
19
19
  import { createTransport, getTransport, listTransports, releaseTransport } from '../adt/transport.js';
20
+ import { getAppInfo } from '../adt/ui5-repository.js';
20
21
  import { validateAffHeader } from '../aff/validator.js';
21
22
  import { extractCdsElements } from '../context/cds-deps.js';
22
23
  import { compressCdsContext, compressContext } from '../context/compressor.js';
@@ -74,6 +75,46 @@ function textResult(text) {
74
75
  function errorResult(message) {
75
76
  return { content: [{ type: 'text', text: message }], isError: true };
76
77
  }
78
+ // ─── Search Helpers ─────────────────────────────────────────────────
79
+ /**
80
+ * Transliterate non-ASCII characters in search queries.
81
+ * SAP object names are ASCII-only, so umlauts and accented characters
82
+ * never appear in object names. This prevents wasted searches with
83
+ * German terms like "*Schätzung*" that silently return empty results.
84
+ */
85
+ export function transliterateQuery(query) {
86
+ // Explicit German umlaut replacements (must come before NFD decomposition)
87
+ let result = query
88
+ .replace(/ä/g, 'AE')
89
+ .replace(/Ä/g, 'AE')
90
+ .replace(/ö/g, 'OE')
91
+ .replace(/Ö/g, 'OE')
92
+ .replace(/ü/g, 'UE')
93
+ .replace(/Ü/g, 'UE')
94
+ .replace(/ß/g, 'SS');
95
+ // General fallback: strip remaining diacritics (é→e, ñ→n, etc.)
96
+ result = result.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
97
+ return { normalized: result, changed: result !== query };
98
+ }
99
+ /**
100
+ * Detect if a search query looks like a field/column name rather than
101
+ * an object name. Field names are short, uppercase, and typically don't
102
+ * start with Z/Y (which are custom object prefixes).
103
+ */
104
+ export function looksLikeFieldName(query) {
105
+ // Wildcard patterns are object searches, not field names
106
+ if (query.includes('*'))
107
+ return false;
108
+ if (query.length === 0 || query.length > 15)
109
+ return false;
110
+ // Must be uppercase letters, digits, underscores only
111
+ if (!/^[A-Z0-9_]+$/.test(query))
112
+ return false;
113
+ // Z/Y prefix → more likely an object name
114
+ if (/^[ZY]/.test(query))
115
+ return false;
116
+ return true;
117
+ }
77
118
  /** Classify error type for audit logging */
78
119
  /** Format error messages with LLM-friendly remediation hints */
79
120
  function formatErrorForLLM(err, message, _tool, args) {
@@ -86,12 +127,49 @@ function formatErrorForLLM(err, message, _tool, args) {
86
127
  if (err.isUnauthorized || err.isForbidden) {
87
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.`;
88
129
  }
130
+ // Transport / corrNr specific hints
131
+ const transportHint = getTransportHint(err);
132
+ if (transportHint) {
133
+ return `${message}\n\nHint: ${transportHint}`;
134
+ }
89
135
  }
90
136
  if (err instanceof AdtNetworkError) {
91
137
  return `${message}\n\nHint: Cannot reach the SAP system. This is a connectivity issue, not a usage error.`;
92
138
  }
93
139
  return message;
94
140
  }
141
+ /** Detect transport/corrNr failure signatures and return a remediation hint, or undefined if not transport-related. */
142
+ function getTransportHint(err) {
143
+ const body = (err.responseBody ?? '').toLowerCase();
144
+ const msg = err.message.toLowerCase();
145
+ const combined = `${msg} ${body}`;
146
+ // Missing or invalid transport/correction number
147
+ if (combined.includes('correction number') ||
148
+ combined.includes('corrnr') ||
149
+ (combined.includes('transport request') &&
150
+ (combined.includes('missing') || combined.includes('required') || combined.includes('invalid')))) {
151
+ 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.';
152
+ }
153
+ // Transport not found or not modifiable
154
+ if (combined.includes('e070') ||
155
+ (combined.includes('transport') &&
156
+ (combined.includes('not found') || combined.includes('does not exist') || combined.includes('not modifiable')))) {
157
+ 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.';
158
+ }
159
+ // Package / transport layer mismatch
160
+ if (combined.includes('transport layer') ||
161
+ (combined.includes('package') &&
162
+ combined.includes('transport') &&
163
+ (combined.includes('mismatch') || combined.includes('not assigned') || combined.includes('no transport layer')))) {
164
+ 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.';
165
+ }
166
+ // Authorization for transport operations
167
+ if (combined.includes('s_transprt') ||
168
+ (combined.includes('transport') && (combined.includes('no authorization') || combined.includes('not authorized')))) {
169
+ return 'The SAP user lacks transport authorization (S_TRANSPRT). Contact your SAP basis administrator to grant the required transport permissions.';
170
+ }
171
+ return undefined;
172
+ }
95
173
  function classifyError(err) {
96
174
  if (err instanceof AdtApiError)
97
175
  return 'AdtApiError';
@@ -292,20 +370,26 @@ async function handleSAPRead(client, args, cachingLayer) {
292
370
  if (isBtpSystem() && BTP_HINTS[type]) {
293
371
  return errorResult(BTP_HINTS[type]);
294
372
  }
295
- // Helper: get source with cache support
373
+ // Helper: get source with cache support, returns cache hit status
296
374
  const cachedGet = async (objType, objName, fetcher) => {
297
375
  if (!cachingLayer)
298
- return fetcher();
299
- const { source } = await cachingLayer.getSource(objType, objName, fetcher);
300
- return source;
376
+ return { source: await fetcher(), cacheHit: false };
377
+ const { source, hit } = await cachingLayer.getSource(objType, objName, fetcher);
378
+ return { source, cacheHit: hit };
379
+ };
380
+ /** Prepend [cached] indicator when result came from cache */
381
+ const cachedTextResult = (source, cacheHit) => {
382
+ return textResult(cacheHit ? `[cached]\n${source}` : source);
301
383
  };
302
384
  // Structured format is only supported for CLAS type
303
385
  if (args.format === 'structured' && type !== 'CLAS') {
304
386
  return errorResult('The "structured" format is only supported for CLAS type. Other types return text format.');
305
387
  }
306
388
  switch (type) {
307
- case 'PROG':
308
- return textResult(await cachedGet('PROG', name, () => client.getProgram(name)));
389
+ case 'PROG': {
390
+ const { source, cacheHit } = await cachedGet('PROG', name, () => client.getProgram(name));
391
+ return cachedTextResult(source, cacheHit);
392
+ }
309
393
  case 'CLAS': {
310
394
  // Structured format: return JSON with metadata + decomposed source
311
395
  if (args.format === 'structured') {
@@ -314,8 +398,8 @@ async function handleSAPRead(client, args, cachingLayer) {
314
398
  }
315
399
  const methodParam = args.method;
316
400
  if (methodParam && !args.include) {
317
- // Method-level read — fetch full source then extract
318
- const fullSource = await cachedGet('CLAS', name, () => client.getClass(name));
401
+ // Method-level read — fetch full source then extract (no cache indicator for derived results)
402
+ const { source: fullSource } = await cachedGet('CLAS', name, () => client.getClass(name));
319
403
  const abaplintVer = cachedFeatures?.abapRelease
320
404
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
321
405
  : undefined;
@@ -331,12 +415,15 @@ async function handleSAPRead(client, args, cachingLayer) {
331
415
  }
332
416
  // Only cache the full merged source (no include param), not individual includes
333
417
  if (!args.include) {
334
- return textResult(await cachedGet('CLAS', name, () => client.getClass(name)));
418
+ const { source, cacheHit } = await cachedGet('CLAS', name, () => client.getClass(name));
419
+ return cachedTextResult(source, cacheHit);
335
420
  }
336
421
  return textResult(await client.getClass(name, args.include));
337
422
  }
338
- case 'INTF':
339
- return textResult(await cachedGet('INTF', name, () => client.getInterface(name)));
423
+ case 'INTF': {
424
+ const { source, cacheHit } = await cachedGet('INTF', name, () => client.getInterface(name));
425
+ return cachedTextResult(source, cacheHit);
426
+ }
340
427
  case 'FUNC': {
341
428
  let group = String(args.group ?? '');
342
429
  if (!group) {
@@ -349,7 +436,8 @@ async function handleSAPRead(client, args, cachingLayer) {
349
436
  }
350
437
  group = resolved;
351
438
  }
352
- return textResult(await cachedGet('FUNC', name, () => client.getFunction(group, name)));
439
+ const { source, cacheHit } = await cachedGet('FUNC', name, () => client.getFunction(group, name));
440
+ return cachedTextResult(source, cacheHit);
353
441
  }
354
442
  case 'FUGR': {
355
443
  const expand = Boolean(args.expand_includes);
@@ -374,22 +462,30 @@ async function handleSAPRead(client, args, cachingLayer) {
374
462
  const fg = await client.getFunctionGroup(name);
375
463
  return textResult(JSON.stringify(fg, null, 2));
376
464
  }
377
- case 'INCL':
378
- return textResult(await cachedGet('INCL', name, () => client.getInclude(name)));
465
+ case 'INCL': {
466
+ const { source, cacheHit } = await cachedGet('INCL', name, () => client.getInclude(name));
467
+ return cachedTextResult(source, cacheHit);
468
+ }
379
469
  case 'DDLS': {
380
- const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
470
+ const { source: ddlSource, cacheHit } = await cachedGet('DDLS', name, () => client.getDdls(name));
381
471
  if (args.include?.toLowerCase() === 'elements') {
472
+ // Elements extraction is derived from source — no cache indicator
382
473
  return textResult(extractCdsElements(ddlSource, name));
383
474
  }
384
- return textResult(ddlSource);
475
+ return cachedTextResult(ddlSource, cacheHit);
476
+ }
477
+ case 'BDEF': {
478
+ const { source, cacheHit } = await cachedGet('BDEF', name, () => client.getBdef(name));
479
+ return cachedTextResult(source, cacheHit);
480
+ }
481
+ case 'SRVD': {
482
+ const { source, cacheHit } = await cachedGet('SRVD', name, () => client.getSrvd(name));
483
+ return cachedTextResult(source, cacheHit);
385
484
  }
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
485
  case 'DDLX': {
391
486
  try {
392
- return textResult(await cachedGet('DDLX', name, () => client.getDdlx(name)));
487
+ const { source, cacheHit } = await cachedGet('DDLX', name, () => client.getDdlx(name));
488
+ return cachedTextResult(source, cacheHit);
393
489
  }
394
490
  catch (err) {
395
491
  if (isNotFoundError(err)) {
@@ -398,14 +494,22 @@ async function handleSAPRead(client, args, cachingLayer) {
398
494
  throw err;
399
495
  }
400
496
  }
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)));
497
+ case 'SRVB': {
498
+ const { source, cacheHit } = await cachedGet('SRVB', name, () => client.getSrvb(name));
499
+ return cachedTextResult(source, cacheHit);
500
+ }
501
+ case 'TABL': {
502
+ const { source, cacheHit } = await cachedGet('TABL', name, () => client.getTable(name));
503
+ return cachedTextResult(source, cacheHit);
504
+ }
505
+ case 'VIEW': {
506
+ const { source, cacheHit } = await cachedGet('VIEW', name, () => client.getView(name));
507
+ return cachedTextResult(source, cacheHit);
508
+ }
509
+ case 'STRU': {
510
+ const { source, cacheHit } = await cachedGet('STRU', name, () => client.getStructure(name));
511
+ return cachedTextResult(source, cacheHit);
512
+ }
409
513
  case 'DOMA': {
410
514
  const domain = await client.getDomain(name);
411
515
  return textResult(JSON.stringify(domain, null, 2));
@@ -431,6 +535,18 @@ async function handleSAPRead(client, args, cachingLayer) {
431
535
  }
432
536
  return textResult(JSON.stringify(tran, null, 2));
433
537
  }
538
+ case 'API_STATE': {
539
+ // Determine object type for URL construction — use explicit objectType, infer from name, or error
540
+ const explicitType = String(args.objectType ?? '').toUpperCase();
541
+ const inferredType = explicitType || inferObjectType(name);
542
+ if (!inferredType) {
543
+ 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").`);
544
+ }
545
+ // Use raw URI (no name encoding) — getApiReleaseState encodes the full URI as a single path segment
546
+ const objectUri = objectUrlForTypeRaw(inferredType, name);
547
+ const releaseState = await client.getApiReleaseState(objectUri);
548
+ return textResult(JSON.stringify(releaseState, null, 2));
549
+ }
434
550
  case 'TABLE_CONTENTS': {
435
551
  const maxRows = Number(args.maxRows ?? 100);
436
552
  const data = await client.getTableContents(name, maxRows, args.sqlFilter);
@@ -480,18 +596,53 @@ async function handleSAPRead(client, args, cachingLayer) {
480
596
  return textResult(await client.getTextElements(name));
481
597
  case 'VARIANTS':
482
598
  return textResult(await client.getVariants(name));
599
+ case 'BSP': {
600
+ if (cachedFeatures?.ui5 && !cachedFeatures.ui5.available) {
601
+ return errorResult('UI5/Fiori BSP Filestore is not available on this SAP system. ' +
602
+ 'Run SAPManage(action="probe") to verify feature availability.');
603
+ }
604
+ const include = args.include;
605
+ if (!name) {
606
+ // List all BSP apps (optional search via query param not used here since name is empty)
607
+ const apps = await client.listBspApps();
608
+ return textResult(JSON.stringify(apps, null, 2));
609
+ }
610
+ if (!include) {
611
+ // Browse root structure of the app
612
+ return textResult(JSON.stringify(await client.getBspAppStructure(name), null, 2));
613
+ }
614
+ // If include contains a dot, treat as file read; otherwise browse subfolder
615
+ if (include.includes('.')) {
616
+ return textResult(await client.getBspFileContent(name, include));
617
+ }
618
+ return textResult(JSON.stringify(await client.getBspAppStructure(name, `/${include}`), null, 2));
619
+ }
620
+ case 'BSP_DEPLOY': {
621
+ if (cachedFeatures?.ui5repo && !cachedFeatures.ui5repo.available) {
622
+ return errorResult('ABAP Repository OData Service is not available on this SAP system. ' +
623
+ 'Run SAPManage(action="probe") to verify feature availability.');
624
+ }
625
+ if (!name) {
626
+ return errorResult('BSP_DEPLOY requires a name parameter (e.g., name="ZAPP_BOOKING").');
627
+ }
628
+ const info = await getAppInfo(client.http, client.safety, name);
629
+ if (!info) {
630
+ return textResult(`App "${name}" not found in ABAP Repository.`);
631
+ }
632
+ return textResult(JSON.stringify(info, null, 2));
633
+ }
483
634
  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. ` +
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. ` +
485
636
  '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
637
  'Do not pass a URI — use the "type" and "name" parameters instead.');
487
638
  }
488
639
  }
489
640
  async function handleSAPSearch(client, args) {
490
- const query = String(args.query ?? '');
641
+ const rawQuery = String(args.query ?? '');
491
642
  const maxResults = Number(args.maxResults ?? 100);
492
643
  const searchType = String(args.searchType ?? 'object');
493
644
  if (searchType === 'source_code') {
494
- // If probe already determined textSearch is unavailable, return the precise reason
645
+ // Source code search: do NOT transliterate source can contain umlauts in strings/comments
495
646
  if (cachedFeatures?.textSearch && !cachedFeatures.textSearch.available) {
496
647
  return errorResult(`Source code search is not available on this SAP system. ${cachedFeatures.textSearch.reason ?? ''}` +
497
648
  `\nUse SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
@@ -499,7 +650,7 @@ async function handleSAPSearch(client, args) {
499
650
  const objectType = args.objectType;
500
651
  const packageName = args.packageName;
501
652
  try {
502
- const results = await client.searchSource(query, maxResults, objectType, packageName);
653
+ const results = await client.searchSource(rawQuery, maxResults, objectType, packageName);
503
654
  return textResult(JSON.stringify(results, null, 2));
504
655
  }
505
656
  catch (err) {
@@ -514,8 +665,25 @@ async function handleSAPSearch(client, args) {
514
665
  throw err;
515
666
  }
516
667
  }
668
+ // Object search: transliterate non-ASCII (SAP object names are ASCII-only)
669
+ const { normalized: query, changed: wasTransliterated } = transliterateQuery(rawQuery);
670
+ const transliterationNote = wasTransliterated
671
+ ? `Note: Query contained non-ASCII characters. Transliterated "${rawQuery}" → "${query}" (SAP object names are ASCII-only).\n\n`
672
+ : '';
517
673
  const results = await client.searchObject(query, maxResults);
518
- return textResult(JSON.stringify(results, null, 2));
674
+ if (Array.isArray(results) && results.length === 0) {
675
+ let hint = '[]' +
676
+ '\n\n' +
677
+ transliterationNote +
678
+ 'No objects found. If searching for custom objects, try Z* or Y* prefixes (e.g., "Z*ESTIM*"). ' +
679
+ 'If you already found objects in a package, use SAPRead with type=DEVC to list all package contents instead of more searches.';
680
+ if (looksLikeFieldName(query)) {
681
+ const stripped = query.replace(/\*/g, '');
682
+ 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.`;
683
+ }
684
+ return textResult(hint);
685
+ }
686
+ return textResult(transliterationNote + JSON.stringify(results, null, 2));
519
687
  }
520
688
  async function handleSAPQuery(client, args) {
521
689
  const sql = String(args.sql ?? '');
@@ -739,46 +907,67 @@ function escapeXml(s) {
739
907
  .replace(/>/g, '>');
740
908
  }
741
909
  // ─── 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);
910
+ /** Base path for an object type. Returns path prefix without trailing name segment. */
911
+ function objectBasePath(type) {
745
912
  switch (type) {
746
913
  case 'PROG':
747
- return `/sap/bc/adt/programs/programs/${encoded}`;
914
+ return '/sap/bc/adt/programs/programs/';
748
915
  case 'CLAS':
749
- return `/sap/bc/adt/oo/classes/${encoded}`;
916
+ return '/sap/bc/adt/oo/classes/';
750
917
  case 'INTF':
751
- return `/sap/bc/adt/oo/interfaces/${encoded}`;
918
+ return '/sap/bc/adt/oo/interfaces/';
752
919
  case 'FUNC':
753
- return `/sap/bc/adt/functions/groups/${encoded}`;
920
+ return '/sap/bc/adt/functions/groups/';
754
921
  case 'INCL':
755
- return `/sap/bc/adt/programs/includes/${encoded}`;
922
+ return '/sap/bc/adt/programs/includes/';
756
923
  case 'FUGR':
757
- return `/sap/bc/adt/functions/groups/${encoded}`;
924
+ return '/sap/bc/adt/functions/groups/';
758
925
  case 'DDLS':
759
- return `/sap/bc/adt/ddic/ddl/sources/${encoded}`;
926
+ return '/sap/bc/adt/ddic/ddl/sources/';
760
927
  case 'BDEF':
761
- return `/sap/bc/adt/bo/behaviordefinitions/${encoded}`;
928
+ return '/sap/bc/adt/bo/behaviordefinitions/';
762
929
  case 'SRVD':
763
- return `/sap/bc/adt/ddic/srvd/sources/${encoded}`;
930
+ return '/sap/bc/adt/ddic/srvd/sources/';
764
931
  case 'DDLX':
765
- return `/sap/bc/adt/ddic/ddlx/sources/${encoded}`;
932
+ return '/sap/bc/adt/ddic/ddlx/sources/';
766
933
  case 'SRVB':
767
- return `/sap/bc/adt/businessservices/bindings/${encoded}`;
934
+ return '/sap/bc/adt/businessservices/bindings/';
768
935
  case 'TABL':
769
- return `/sap/bc/adt/ddic/tables/${encoded}`;
936
+ return '/sap/bc/adt/ddic/tables/';
770
937
  case 'STRU':
771
- return `/sap/bc/adt/ddic/structures/${encoded}`;
938
+ return '/sap/bc/adt/ddic/structures/';
772
939
  case 'DOMA':
773
- return `/sap/bc/adt/ddic/domains/${encoded}`;
940
+ return '/sap/bc/adt/ddic/domains/';
774
941
  case 'DTEL':
775
- return `/sap/bc/adt/ddic/dataelements/${encoded}`;
942
+ return '/sap/bc/adt/ddic/dataelements/';
776
943
  case 'TRAN':
777
- return `/sap/bc/adt/vit/wb/object_type/trant/object_name/${encoded}`;
944
+ return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
778
945
  default:
779
- return `/sap/bc/adt/programs/programs/${encoded}`;
946
+ return '/sap/bc/adt/programs/programs/';
780
947
  }
781
948
  }
949
+ /** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. Name is URI-encoded. */
950
+ function objectUrlForType(type, name) {
951
+ return `${objectBasePath(type)}${encodeURIComponent(name)}`;
952
+ }
953
+ /** Infer SAP object type from naming conventions. Returns empty string if type cannot be determined. */
954
+ function inferObjectType(name) {
955
+ const upper = name.toUpperCase();
956
+ if (upper.startsWith('IF_') || upper.startsWith('ZIF_') || upper.startsWith('YIF_'))
957
+ return 'INTF';
958
+ if (upper.startsWith('CL_') || upper.startsWith('ZCL_') || upper.startsWith('YCL_'))
959
+ return 'CLAS';
960
+ if (upper.startsWith('CX_') || upper.startsWith('ZCX_') || upper.startsWith('YCX_'))
961
+ return 'CLAS';
962
+ return '';
963
+ }
964
+ /**
965
+ * Map object type + name to the ADT object URL WITHOUT encoding the name.
966
+ * Used for API release state where the full URI is encoded as a single path segment by the caller.
967
+ */
968
+ function objectUrlForTypeRaw(type, name) {
969
+ return `${objectBasePath(type)}${name}`;
970
+ }
782
971
  /** Get the source URL for an object (appends /source/main) */
783
972
  function sourceUrlForType(type, name) {
784
973
  return `${objectUrlForType(type, name)}/source/main`;
@@ -811,7 +1000,6 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
811
1000
  const pkg = String(args.package ?? '$TMP');
812
1001
  checkPackage(client.safety, pkg);
813
1002
  const description = String(args.description ?? name);
814
- checkPackage(client.safety, pkg);
815
1003
  // AFF header validation (if schema available for this type)
816
1004
  const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
817
1005
  if (!affResult.valid) {
@@ -870,11 +1058,12 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
870
1058
  return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
871
1059
  }
872
1060
  case 'delete': {
873
- // Lock, delete, unlock pattern
1061
+ // Lock, delete, unlock pattern — auto-propagate lock corrNr if no explicit transport
874
1062
  await client.http.withStatefulSession(async (session) => {
875
1063
  const lock = await lockObject(session, client.safety, objectUrl);
1064
+ const effectiveTransport = transport ?? (lock.corrNr || undefined);
876
1065
  try {
877
- await deleteObject(session, client.safety, objectUrl, lock.lockHandle, transport);
1066
+ await deleteObject(session, client.safety, objectUrl, lock.lockHandle, effectiveTransport);
878
1067
  }
879
1068
  finally {
880
1069
  try {
@@ -1039,8 +1228,78 @@ function runPreWriteLint(source, type, name, config) {
1039
1228
  }
1040
1229
  // ─── SAPActivate Handler ─────────────────────────────────────────────
1041
1230
  async function handleSAPActivate(client, args) {
1042
- const type = String(args.type ?? '');
1231
+ const action = String(args.action ?? 'activate');
1232
+ const name = String(args.name ?? '');
1233
+ const version = String(args.version ?? '0001');
1234
+ // Publish service binding
1235
+ if (action === 'publish_srvb') {
1236
+ if (!name) {
1237
+ return errorResult('Missing required "name" parameter for publish_srvb action.');
1238
+ }
1239
+ const result = await publishServiceBinding(client.http, client.safety, name, version);
1240
+ if (result.severity === 'ERROR') {
1241
+ return errorResult(`Failed to publish service binding ${name}: ${result.shortText}${result.longText ? ` — ${result.longText}` : ''}`);
1242
+ }
1243
+ let srvbInfo;
1244
+ try {
1245
+ srvbInfo = await client.getSrvb(name);
1246
+ }
1247
+ catch {
1248
+ if (result.severity === 'UNKNOWN') {
1249
+ return errorResult(`Publish response for ${name} could not be parsed and readback failed — use SAPRead to verify publish status.`);
1250
+ }
1251
+ return textResult(`Successfully published service binding ${name} (readback of binding metadata failed — use SAPRead to verify)`);
1252
+ }
1253
+ // Verify the published flag from the SRVB readback
1254
+ try {
1255
+ const srvbData = JSON.parse(srvbInfo);
1256
+ if (srvbData.published === false) {
1257
+ return errorResult(`Publish of service binding ${name} may have failed — binding is still unpublished.\n\n${srvbInfo}`);
1258
+ }
1259
+ }
1260
+ catch {
1261
+ // If we can't parse the readback JSON, fall through — better to return what we have
1262
+ }
1263
+ if (result.severity === 'UNKNOWN') {
1264
+ return textResult(`Publish request for ${name} completed but response could not be fully parsed. Verify status below:\n\n${srvbInfo}`);
1265
+ }
1266
+ return textResult(`Successfully published service binding ${name}.\n\n${srvbInfo}`);
1267
+ }
1268
+ // Unpublish service binding
1269
+ if (action === 'unpublish_srvb') {
1270
+ if (!name) {
1271
+ return errorResult('Missing required "name" parameter for unpublish_srvb action.');
1272
+ }
1273
+ const result = await unpublishServiceBinding(client.http, client.safety, name, version);
1274
+ if (result.severity === 'ERROR') {
1275
+ return errorResult(`Failed to unpublish service binding ${name}: ${result.shortText}${result.longText ? ` — ${result.longText}` : ''}`);
1276
+ }
1277
+ let srvbInfo;
1278
+ try {
1279
+ srvbInfo = await client.getSrvb(name);
1280
+ }
1281
+ catch {
1282
+ // Readback failed — fall through with what we have
1283
+ }
1284
+ // Verify the published flag from the SRVB readback
1285
+ if (srvbInfo) {
1286
+ try {
1287
+ const srvbData = JSON.parse(srvbInfo);
1288
+ if (srvbData.published === true) {
1289
+ return errorResult(`Unpublish of service binding ${name} may have failed — binding is still published.\n\n${srvbInfo}`);
1290
+ }
1291
+ }
1292
+ catch {
1293
+ // If we can't parse the readback JSON, fall through
1294
+ }
1295
+ }
1296
+ if (result.severity === 'UNKNOWN') {
1297
+ 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.'}`);
1298
+ }
1299
+ return textResult(`Successfully unpublished service binding ${name}.${srvbInfo ? `\n\n${srvbInfo}` : ''}`);
1300
+ }
1043
1301
  // Batch activation: multiple objects at once (for RAP stacks etc.)
1302
+ const type = String(args.type ?? '');
1044
1303
  if (args.objects && Array.isArray(args.objects)) {
1045
1304
  const objects = args.objects.map((o) => {
1046
1305
  const objType = String(o.type ?? type);
@@ -1055,7 +1314,6 @@ async function handleSAPActivate(client, args) {
1055
1314
  return errorResult(`Batch activation failed for: ${names}.\nErrors: ${result.messages.join('; ')}`);
1056
1315
  }
1057
1316
  // Single activation (existing behavior)
1058
- const name = String(args.name ?? '');
1059
1317
  const objectUrl = objectUrlForType(type, name);
1060
1318
  const result = await activate(client.http, client.safety, objectUrl);
1061
1319
  if (result.success) {
@@ -1137,8 +1395,63 @@ async function handleSAPNavigate(client, args) {
1137
1395
  const proposals = await getCompletion(client.http, client.safety, uri, line, column, source);
1138
1396
  return textResult(JSON.stringify(proposals, null, 2));
1139
1397
  }
1398
+ case 'hierarchy': {
1399
+ const className = String(args.name ?? '').toUpperCase();
1400
+ if (!className) {
1401
+ return errorResult('Provide name (class name) for hierarchy lookup.');
1402
+ }
1403
+ // Sanitize to prevent SQL injection — class names are alphanumeric + underscore + namespace slash
1404
+ const safeName = className.replace(/[^A-Z0-9_/]/g, '');
1405
+ if (safeName !== className) {
1406
+ return errorResult(`Invalid class name: "${className}". Only alphanumeric characters, underscores, and slashes are allowed.`);
1407
+ }
1408
+ const canFreeSQL = isOperationAllowed(client.safety, OperationType.FreeSQL);
1409
+ const canQuery = isOperationAllowed(client.safety, OperationType.Query);
1410
+ if (!canFreeSQL && !canQuery) {
1411
+ return errorResult('Class hierarchy requires data access permissions. ' +
1412
+ 'Enable free SQL (--block-free-sql=false) or table preview (--block-data=false).');
1413
+ }
1414
+ try {
1415
+ let ownRels;
1416
+ let subRels;
1417
+ if (canFreeSQL) {
1418
+ ownRels = await client.runQuery(`SELECT CLSNAME, REFCLSNAME, RELTYPE FROM SEOMETAREL WHERE CLSNAME = '${safeName}'`, 100);
1419
+ subRels = await client.runQuery(`SELECT CLSNAME FROM SEOMETAREL WHERE REFCLSNAME = '${safeName}' AND RELTYPE = '2'`, 100);
1420
+ }
1421
+ else {
1422
+ // Fall back to named table preview (Query op type)
1423
+ ownRels = await client.getTableContents('SEOMETAREL', 100, `CLSNAME = '${safeName}'`);
1424
+ subRels = await client.getTableContents('SEOMETAREL', 100, `REFCLSNAME = '${safeName}' AND RELTYPE = '2'`);
1425
+ }
1426
+ let superclass = null;
1427
+ const interfaces = [];
1428
+ for (let i = 0; i < ownRels.rows.length; i++) {
1429
+ const row = ownRels.rows[i];
1430
+ const reltype = String(row.RELTYPE ?? '').trim();
1431
+ const refName = String(row.REFCLSNAME ?? '').trim();
1432
+ if (reltype === '2') {
1433
+ superclass = refName;
1434
+ }
1435
+ else if (reltype === '1') {
1436
+ interfaces.push(refName);
1437
+ }
1438
+ }
1439
+ const subclasses = [];
1440
+ for (let i = 0; i < subRels.rows.length; i++) {
1441
+ subclasses.push(String(subRels.rows[i].CLSNAME ?? '').trim());
1442
+ }
1443
+ const result = { className: safeName, superclass, interfaces, subclasses };
1444
+ return textResult(JSON.stringify(result, null, 2));
1445
+ }
1446
+ catch (err) {
1447
+ if (err instanceof AdtApiError && err.statusCode === 404) {
1448
+ return errorResult('Cannot query SEOMETAREL — table may not be accessible on this system.');
1449
+ }
1450
+ throw err;
1451
+ }
1452
+ }
1140
1453
  default:
1141
- return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion`);
1454
+ return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion, hierarchy`);
1142
1455
  }
1143
1456
  }
1144
1457
  // ─── SAPDiagnose Handler ─────────────────────────────────────────────
@@ -1229,6 +1542,8 @@ async function handleSAPTransport(client, args) {
1229
1542
  if (!description)
1230
1543
  return errorResult('Description is required for "create" action.');
1231
1544
  const id = await createTransport(client.http, client.safety, description);
1545
+ if (!id)
1546
+ return errorResult('Transport creation succeeded but no transport ID was returned. Check the SAP system manually.');
1232
1547
  return textResult(`Created transport request: ${id}`);
1233
1548
  }
1234
1549
  case 'release': {
@@ -1382,6 +1697,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
1382
1697
  featureConfig.amdp = config.featureAmdp;
1383
1698
  featureConfig.ui5 = config.featureUi5;
1384
1699
  featureConfig.transport = config.featureTransport;
1700
+ featureConfig.ui5repo = config.featureUi5Repo;
1385
1701
  const probed = await probeFeatures(client.http, featureConfig, config.systemType);
1386
1702
  // In PP mode with a per-user client, auth-sensitive results (401/403 on any
1387
1703
  // feature) must not poison the global cache — another user may have different