arc-1 0.5.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 (79) hide show
  1. package/README.md +13 -12
  2. package/dist/adt/client.d.ts +13 -1
  3. package/dist/adt/client.d.ts.map +1 -1
  4. package/dist/adt/client.js +77 -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 +3 -8
  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/safety.d.ts +0 -7
  27. package/dist/adt/safety.d.ts.map +1 -1
  28. package/dist/adt/safety.js +3 -49
  29. package/dist/adt/safety.js.map +1 -1
  30. package/dist/adt/transport.d.ts +6 -0
  31. package/dist/adt/transport.d.ts.map +1 -1
  32. package/dist/adt/transport.js +35 -22
  33. package/dist/adt/transport.js.map +1 -1
  34. package/dist/adt/types.d.ts +68 -0
  35. package/dist/adt/types.d.ts.map +1 -1
  36. package/dist/adt/ui5-repository.d.ts +19 -0
  37. package/dist/adt/ui5-repository.d.ts.map +1 -0
  38. package/dist/adt/ui5-repository.js +43 -0
  39. package/dist/adt/ui5-repository.js.map +1 -0
  40. package/dist/adt/xml-parser.d.ts +39 -1
  41. package/dist/adt/xml-parser.d.ts.map +1 -1
  42. package/dist/adt/xml-parser.js +185 -2
  43. package/dist/adt/xml-parser.js.map +1 -1
  44. package/dist/aff/schemas/bdef-v1.json +62 -0
  45. package/dist/aff/schemas/clas-v1.json +276 -0
  46. package/dist/aff/schemas/ddls-v1.json +144 -0
  47. package/dist/aff/schemas/intf-v1.json +243 -0
  48. package/dist/aff/schemas/prog-v1.json +133 -0
  49. package/dist/aff/schemas/srvb-v1.json +115 -0
  50. package/dist/aff/schemas/srvd-v1.json +108 -0
  51. package/dist/aff/validator.d.ts +14 -0
  52. package/dist/aff/validator.d.ts.map +1 -0
  53. package/dist/aff/validator.js +83 -0
  54. package/dist/aff/validator.js.map +1 -0
  55. package/dist/handlers/hyperfocused.js +1 -1
  56. package/dist/handlers/hyperfocused.js.map +1 -1
  57. package/dist/handlers/intent.d.ts +16 -0
  58. package/dist/handlers/intent.d.ts.map +1 -1
  59. package/dist/handlers/intent.js +495 -62
  60. package/dist/handlers/intent.js.map +1 -1
  61. package/dist/handlers/schemas.d.ts +80 -24
  62. package/dist/handlers/schemas.d.ts.map +1 -1
  63. package/dist/handlers/schemas.js +34 -6
  64. package/dist/handlers/schemas.js.map +1 -1
  65. package/dist/handlers/tools.d.ts.map +1 -1
  66. package/dist/handlers/tools.js +81 -19
  67. package/dist/handlers/tools.js.map +1 -1
  68. package/dist/server/config.d.ts.map +1 -1
  69. package/dist/server/config.js +7 -14
  70. package/dist/server/config.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 +2 -2
  74. package/dist/server/server.js.map +1 -1
  75. package/dist/server/types.d.ts +1 -1
  76. package/dist/server/types.d.ts.map +1 -1
  77. package/dist/server/types.js +2 -2
  78. package/dist/server/types.js.map +1 -1
  79. package/package.json +14 -4
@@ -11,12 +11,14 @@
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
- import { isOperationAllowed, OperationType } from '../adt/safety.js';
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';
21
+ import { validateAffHeader } from '../aff/validator.js';
20
22
  import { extractCdsElements } from '../context/cds-deps.js';
21
23
  import { compressCdsContext, compressContext } from '../context/compressor.js';
22
24
  import { extractMethod, formatMethodListing, listMethods, spliceMethod } from '../context/method-surgery.js';
@@ -73,6 +75,46 @@ function textResult(text) {
73
75
  function errorResult(message) {
74
76
  return { content: [{ type: 'text', text: message }], isError: true };
75
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
+ }
76
118
  /** Classify error type for audit logging */
77
119
  /** Format error messages with LLM-friendly remediation hints */
78
120
  function formatErrorForLLM(err, message, _tool, args) {
@@ -85,12 +127,49 @@ function formatErrorForLLM(err, message, _tool, args) {
85
127
  if (err.isUnauthorized || err.isForbidden) {
86
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.`;
87
129
  }
130
+ // Transport / corrNr specific hints
131
+ const transportHint = getTransportHint(err);
132
+ if (transportHint) {
133
+ return `${message}\n\nHint: ${transportHint}`;
134
+ }
88
135
  }
89
136
  if (err instanceof AdtNetworkError) {
90
137
  return `${message}\n\nHint: Cannot reach the SAP system. This is a connectivity issue, not a usage error.`;
91
138
  }
92
139
  return message;
93
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
+ }
94
173
  function classifyError(err) {
95
174
  if (err instanceof AdtApiError)
96
175
  return 'AdtApiError';
@@ -291,21 +370,36 @@ async function handleSAPRead(client, args, cachingLayer) {
291
370
  if (isBtpSystem() && BTP_HINTS[type]) {
292
371
  return errorResult(BTP_HINTS[type]);
293
372
  }
294
- // Helper: get source with cache support
373
+ // Helper: get source with cache support, returns cache hit status
295
374
  const cachedGet = async (objType, objName, fetcher) => {
296
375
  if (!cachingLayer)
297
- return fetcher();
298
- const { source } = await cachingLayer.getSource(objType, objName, fetcher);
299
- return source;
376
+ return { source: await fetcher(), cacheHit: false };
377
+ const { source, hit } = await cachingLayer.getSource(objType, objName, fetcher);
378
+ return { source, cacheHit: hit };
300
379
  };
380
+ /** Prepend [cached] indicator when result came from cache */
381
+ const cachedTextResult = (source, cacheHit) => {
382
+ return textResult(cacheHit ? `[cached]\n${source}` : source);
383
+ };
384
+ // Structured format is only supported for CLAS type
385
+ if (args.format === 'structured' && type !== 'CLAS') {
386
+ return errorResult('The "structured" format is only supported for CLAS type. Other types return text format.');
387
+ }
301
388
  switch (type) {
302
- case 'PROG':
303
- 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
+ }
304
393
  case 'CLAS': {
394
+ // Structured format: return JSON with metadata + decomposed source
395
+ if (args.format === 'structured') {
396
+ const structured = await client.getClassStructured(name);
397
+ return textResult(JSON.stringify(structured, null, 2));
398
+ }
305
399
  const methodParam = args.method;
306
400
  if (methodParam && !args.include) {
307
- // Method-level read — fetch full source then extract
308
- 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));
309
403
  const abaplintVer = cachedFeatures?.abapRelease
310
404
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
311
405
  : undefined;
@@ -321,12 +415,15 @@ async function handleSAPRead(client, args, cachingLayer) {
321
415
  }
322
416
  // Only cache the full merged source (no include param), not individual includes
323
417
  if (!args.include) {
324
- 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);
325
420
  }
326
421
  return textResult(await client.getClass(name, args.include));
327
422
  }
328
- case 'INTF':
329
- 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
+ }
330
427
  case 'FUNC': {
331
428
  let group = String(args.group ?? '');
332
429
  if (!group) {
@@ -339,7 +436,8 @@ async function handleSAPRead(client, args, cachingLayer) {
339
436
  }
340
437
  group = resolved;
341
438
  }
342
- 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);
343
441
  }
344
442
  case 'FUGR': {
345
443
  const expand = Boolean(args.expand_includes);
@@ -364,22 +462,30 @@ async function handleSAPRead(client, args, cachingLayer) {
364
462
  const fg = await client.getFunctionGroup(name);
365
463
  return textResult(JSON.stringify(fg, null, 2));
366
464
  }
367
- case 'INCL':
368
- 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
+ }
369
469
  case 'DDLS': {
370
- const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
470
+ const { source: ddlSource, cacheHit } = await cachedGet('DDLS', name, () => client.getDdls(name));
371
471
  if (args.include?.toLowerCase() === 'elements') {
472
+ // Elements extraction is derived from source — no cache indicator
372
473
  return textResult(extractCdsElements(ddlSource, name));
373
474
  }
374
- 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);
375
484
  }
376
- case 'BDEF':
377
- return textResult(await cachedGet('BDEF', name, () => client.getBdef(name)));
378
- case 'SRVD':
379
- return textResult(await cachedGet('SRVD', name, () => client.getSrvd(name)));
380
485
  case 'DDLX': {
381
486
  try {
382
- 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);
383
489
  }
384
490
  catch (err) {
385
491
  if (isNotFoundError(err)) {
@@ -388,14 +494,22 @@ async function handleSAPRead(client, args, cachingLayer) {
388
494
  throw err;
389
495
  }
390
496
  }
391
- case 'SRVB':
392
- return textResult(await cachedGet('SRVB', name, () => client.getSrvb(name)));
393
- case 'TABL':
394
- return textResult(await cachedGet('TABL', name, () => client.getTable(name)));
395
- case 'VIEW':
396
- return textResult(await cachedGet('VIEW', name, () => client.getView(name)));
397
- case 'STRU':
398
- 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
+ }
399
513
  case 'DOMA': {
400
514
  const domain = await client.getDomain(name);
401
515
  return textResult(JSON.stringify(domain, null, 2));
@@ -421,6 +535,18 @@ async function handleSAPRead(client, args, cachingLayer) {
421
535
  }
422
536
  return textResult(JSON.stringify(tran, null, 2));
423
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
+ }
424
550
  case 'TABLE_CONTENTS': {
425
551
  const maxRows = Number(args.maxRows ?? 100);
426
552
  const data = await client.getTableContents(name, maxRows, args.sqlFilter);
@@ -470,18 +596,53 @@ async function handleSAPRead(client, args, cachingLayer) {
470
596
  return textResult(await client.getTextElements(name));
471
597
  case 'VARIANTS':
472
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
+ }
473
634
  default:
474
- 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. ` +
475
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"). ' +
476
637
  'Do not pass a URI — use the "type" and "name" parameters instead.');
477
638
  }
478
639
  }
479
640
  async function handleSAPSearch(client, args) {
480
- const query = String(args.query ?? '');
641
+ const rawQuery = String(args.query ?? '');
481
642
  const maxResults = Number(args.maxResults ?? 100);
482
643
  const searchType = String(args.searchType ?? 'object');
483
644
  if (searchType === 'source_code') {
484
- // 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
485
646
  if (cachedFeatures?.textSearch && !cachedFeatures.textSearch.available) {
486
647
  return errorResult(`Source code search is not available on this SAP system. ${cachedFeatures.textSearch.reason ?? ''}` +
487
648
  `\nUse SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
@@ -489,7 +650,7 @@ async function handleSAPSearch(client, args) {
489
650
  const objectType = args.objectType;
490
651
  const packageName = args.packageName;
491
652
  try {
492
- const results = await client.searchSource(query, maxResults, objectType, packageName);
653
+ const results = await client.searchSource(rawQuery, maxResults, objectType, packageName);
493
654
  return textResult(JSON.stringify(results, null, 2));
494
655
  }
495
656
  catch (err) {
@@ -504,8 +665,25 @@ async function handleSAPSearch(client, args) {
504
665
  throw err;
505
666
  }
506
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
+ : '';
507
673
  const results = await client.searchObject(query, maxResults);
508
- 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));
509
687
  }
510
688
  async function handleSAPQuery(client, args) {
511
689
  const sql = String(args.sql ?? '');
@@ -729,46 +907,67 @@ function escapeXml(s) {
729
907
  .replace(/>/g, '>');
730
908
  }
731
909
  // ─── Object URL Mapping ──────────────────────────────────────────────
732
- /** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. */
733
- function objectUrlForType(type, name) {
734
- const encoded = encodeURIComponent(name);
910
+ /** Base path for an object type. Returns path prefix without trailing name segment. */
911
+ function objectBasePath(type) {
735
912
  switch (type) {
736
913
  case 'PROG':
737
- return `/sap/bc/adt/programs/programs/${encoded}`;
914
+ return '/sap/bc/adt/programs/programs/';
738
915
  case 'CLAS':
739
- return `/sap/bc/adt/oo/classes/${encoded}`;
916
+ return '/sap/bc/adt/oo/classes/';
740
917
  case 'INTF':
741
- return `/sap/bc/adt/oo/interfaces/${encoded}`;
918
+ return '/sap/bc/adt/oo/interfaces/';
742
919
  case 'FUNC':
743
- return `/sap/bc/adt/functions/groups/${encoded}`;
920
+ return '/sap/bc/adt/functions/groups/';
744
921
  case 'INCL':
745
- return `/sap/bc/adt/programs/includes/${encoded}`;
922
+ return '/sap/bc/adt/programs/includes/';
746
923
  case 'FUGR':
747
- return `/sap/bc/adt/functions/groups/${encoded}`;
924
+ return '/sap/bc/adt/functions/groups/';
748
925
  case 'DDLS':
749
- return `/sap/bc/adt/ddic/ddl/sources/${encoded}`;
926
+ return '/sap/bc/adt/ddic/ddl/sources/';
750
927
  case 'BDEF':
751
- return `/sap/bc/adt/bo/behaviordefinitions/${encoded}`;
928
+ return '/sap/bc/adt/bo/behaviordefinitions/';
752
929
  case 'SRVD':
753
- return `/sap/bc/adt/ddic/srvd/sources/${encoded}`;
930
+ return '/sap/bc/adt/ddic/srvd/sources/';
754
931
  case 'DDLX':
755
- return `/sap/bc/adt/ddic/ddlx/sources/${encoded}`;
932
+ return '/sap/bc/adt/ddic/ddlx/sources/';
756
933
  case 'SRVB':
757
- return `/sap/bc/adt/businessservices/bindings/${encoded}`;
934
+ return '/sap/bc/adt/businessservices/bindings/';
758
935
  case 'TABL':
759
- return `/sap/bc/adt/ddic/tables/${encoded}`;
936
+ return '/sap/bc/adt/ddic/tables/';
760
937
  case 'STRU':
761
- return `/sap/bc/adt/ddic/structures/${encoded}`;
938
+ return '/sap/bc/adt/ddic/structures/';
762
939
  case 'DOMA':
763
- return `/sap/bc/adt/ddic/domains/${encoded}`;
940
+ return '/sap/bc/adt/ddic/domains/';
764
941
  case 'DTEL':
765
- return `/sap/bc/adt/ddic/dataelements/${encoded}`;
942
+ return '/sap/bc/adt/ddic/dataelements/';
766
943
  case 'TRAN':
767
- return `/sap/bc/adt/vit/wb/object_type/trant/object_name/${encoded}`;
944
+ return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
768
945
  default:
769
- return `/sap/bc/adt/programs/programs/${encoded}`;
946
+ return '/sap/bc/adt/programs/programs/';
770
947
  }
771
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
+ }
772
971
  /** Get the source URL for an object (appends /source/main) */
773
972
  function sourceUrlForType(type, name) {
774
973
  return `${objectUrlForType(type, name)}/source/main`;
@@ -780,6 +979,10 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
780
979
  const name = String(args.name ?? '');
781
980
  const source = String(args.source ?? '');
782
981
  const transport = args.transport;
982
+ // type and name are required for all actions except batch_create
983
+ if (action !== 'batch_create' && (!type || !name)) {
984
+ return errorResult('"type" and "name" are required for this action.');
985
+ }
783
986
  const objectUrl = objectUrlForType(type, name);
784
987
  const srcUrl = sourceUrlForType(type, name);
785
988
  switch (action) {
@@ -795,7 +998,13 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
795
998
  }
796
999
  case 'create': {
797
1000
  const pkg = String(args.package ?? '$TMP');
1001
+ checkPackage(client.safety, pkg);
798
1002
  const description = String(args.description ?? name);
1003
+ // AFF header validation (if schema available for this type)
1004
+ const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
1005
+ if (!affResult.valid) {
1006
+ return errorResult(`AFF metadata validation failed for ${type} ${name}:\n- ${(affResult.errors ?? []).join('\n- ')}\n\nFix the metadata and retry.`);
1007
+ }
799
1008
  // Build type-specific creation XML body.
800
1009
  // SAP ADT requires the root element to match the object type —
801
1010
  // a generic objectReferences body returns 400 "System expected the element ...".
@@ -849,11 +1058,12 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
849
1058
  return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
850
1059
  }
851
1060
  case 'delete': {
852
- // Lock, delete, unlock pattern
1061
+ // Lock, delete, unlock pattern — auto-propagate lock corrNr if no explicit transport
853
1062
  await client.http.withStatefulSession(async (session) => {
854
1063
  const lock = await lockObject(session, client.safety, objectUrl);
1064
+ const effectiveTransport = transport ?? (lock.corrNr || undefined);
855
1065
  try {
856
- await deleteObject(session, client.safety, objectUrl, lock.lockHandle, transport);
1066
+ await deleteObject(session, client.safety, objectUrl, lock.lockHandle, effectiveTransport);
857
1067
  }
858
1068
  finally {
859
1069
  try {
@@ -867,8 +1077,104 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
867
1077
  cachingLayer?.invalidate(type, name);
868
1078
  return textResult(`Deleted ${type} ${name}.`);
869
1079
  }
1080
+ case 'batch_create': {
1081
+ const objects = args.objects;
1082
+ if (!objects || !Array.isArray(objects) || objects.length === 0) {
1083
+ return errorResult('"objects" array is required and must be non-empty for batch_create action.');
1084
+ }
1085
+ const pkg = String(args.package ?? '$TMP');
1086
+ // Check package is allowed before starting any creates
1087
+ checkPackage(client.safety, pkg);
1088
+ const results = [];
1089
+ for (const obj of objects) {
1090
+ const objType = String(obj.type ?? '');
1091
+ const objName = String(obj.name ?? '');
1092
+ const objSource = obj.source ? String(obj.source) : undefined;
1093
+ const objDescription = String(obj.description ?? objName);
1094
+ // AFF header validation per object (if schema available)
1095
+ const affResult = validateAffHeader(objType, { description: objDescription, originalLanguage: 'en' });
1096
+ if (!affResult.valid) {
1097
+ results.push({
1098
+ type: objType,
1099
+ name: objName,
1100
+ status: 'failed',
1101
+ error: `AFF metadata validation failed:\n- ${(affResult.errors ?? []).join('\n- ')}`,
1102
+ });
1103
+ break;
1104
+ }
1105
+ try {
1106
+ // Pre-validate source with lint BEFORE creating the object to avoid orphaned objects
1107
+ if (objSource) {
1108
+ const lintWarnings = runPreWriteLint(objSource, objType, objName, config);
1109
+ if (lintWarnings.blocked) {
1110
+ results.push({
1111
+ type: objType,
1112
+ name: objName,
1113
+ status: 'failed',
1114
+ error: `source rejected by lint: ${lintWarnings.result.content[0].text}`,
1115
+ });
1116
+ break;
1117
+ }
1118
+ }
1119
+ // Step 1: Create the object
1120
+ const objUrl = objectUrlForType(objType, objName);
1121
+ const createUrl = objUrl.replace(/\/[^/]+$/, '');
1122
+ const body = buildCreateXml(objType, objName, pkg, objDescription);
1123
+ await createObject(client.http, client.safety, createUrl, body, 'application/xml', transport);
1124
+ // Step 2: Write source if provided
1125
+ if (objSource) {
1126
+ const srcUrl = sourceUrlForType(objType, objName);
1127
+ await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, transport);
1128
+ }
1129
+ // Step 3: Activate the object
1130
+ const activationResult = await activate(client.http, client.safety, objUrl);
1131
+ if (!activationResult.success) {
1132
+ results.push({
1133
+ type: objType,
1134
+ name: objName,
1135
+ status: 'failed',
1136
+ error: `activation failed: ${activationResult.messages.join('; ')}`,
1137
+ });
1138
+ break;
1139
+ }
1140
+ cachingLayer?.invalidate(objType, objName);
1141
+ results.push({ type: objType, name: objName, status: 'success' });
1142
+ }
1143
+ catch (err) {
1144
+ results.push({
1145
+ type: objType,
1146
+ name: objName,
1147
+ status: 'failed',
1148
+ error: err instanceof Error ? err.message : String(err),
1149
+ });
1150
+ break;
1151
+ }
1152
+ }
1153
+ // Add 'skipped' entries for objects that were never attempted due to early break
1154
+ for (let i = results.length; i < objects.length; i++) {
1155
+ const skipped = objects[i];
1156
+ results.push({
1157
+ type: String(skipped.type ?? ''),
1158
+ name: String(skipped.name ?? ''),
1159
+ status: 'failed',
1160
+ error: 'skipped — stopped after previous failure',
1161
+ });
1162
+ }
1163
+ const summary = results
1164
+ .map((r) => `${r.name} (${r.type}) ${r.status === 'success' ? '✓' : `✗ — ${r.error}`}`)
1165
+ .join(', ');
1166
+ const successCount = results.filter((r) => r.status === 'success').length;
1167
+ const hasFailure = results.some((r) => r.status === 'failed');
1168
+ if (hasFailure) {
1169
+ const cleanupHint = successCount > 0
1170
+ ? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
1171
+ : '';
1172
+ return errorResult(`Batch created ${successCount}/${objects.length} objects in package ${pkg}: ${summary}${cleanupHint}`);
1173
+ }
1174
+ return textResult(`Batch created ${successCount} objects in package ${pkg}: ${summary}`);
1175
+ }
870
1176
  default:
871
- return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method`);
1177
+ return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create`);
872
1178
  }
873
1179
  }
874
1180
  /**
@@ -922,8 +1228,78 @@ function runPreWriteLint(source, type, name, config) {
922
1228
  }
923
1229
  // ─── SAPActivate Handler ─────────────────────────────────────────────
924
1230
  async function handleSAPActivate(client, args) {
925
- 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
+ }
926
1301
  // Batch activation: multiple objects at once (for RAP stacks etc.)
1302
+ const type = String(args.type ?? '');
927
1303
  if (args.objects && Array.isArray(args.objects)) {
928
1304
  const objects = args.objects.map((o) => {
929
1305
  const objType = String(o.type ?? type);
@@ -938,7 +1314,6 @@ async function handleSAPActivate(client, args) {
938
1314
  return errorResult(`Batch activation failed for: ${names}.\nErrors: ${result.messages.join('; ')}`);
939
1315
  }
940
1316
  // Single activation (existing behavior)
941
- const name = String(args.name ?? '');
942
1317
  const objectUrl = objectUrlForType(type, name);
943
1318
  const result = await activate(client.http, client.safety, objectUrl);
944
1319
  if (result.success) {
@@ -1020,8 +1395,63 @@ async function handleSAPNavigate(client, args) {
1020
1395
  const proposals = await getCompletion(client.http, client.safety, uri, line, column, source);
1021
1396
  return textResult(JSON.stringify(proposals, null, 2));
1022
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
+ }
1023
1453
  default:
1024
- return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion`);
1454
+ return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion, hierarchy`);
1025
1455
  }
1026
1456
  }
1027
1457
  // ─── SAPDiagnose Handler ─────────────────────────────────────────────
@@ -1112,6 +1542,8 @@ async function handleSAPTransport(client, args) {
1112
1542
  if (!description)
1113
1543
  return errorResult('Description is required for "create" action.');
1114
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.');
1115
1547
  return textResult(`Created transport request: ${id}`);
1116
1548
  }
1117
1549
  case 'release': {
@@ -1265,6 +1697,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
1265
1697
  featureConfig.amdp = config.featureAmdp;
1266
1698
  featureConfig.ui5 = config.featureUi5;
1267
1699
  featureConfig.transport = config.featureTransport;
1700
+ featureConfig.ui5repo = config.featureUi5Repo;
1268
1701
  const probed = await probeFeatures(client.http, featureConfig, config.systemType);
1269
1702
  // In PP mode with a per-user client, auth-sensitive results (401/403 on any
1270
1703
  // feature) must not poison the global cache — another user may have different