arc-1 0.6.7 → 0.6.9

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 (73) hide show
  1. package/README.md +15 -11
  2. package/dist/adt/cds-impact.d.ts +25 -0
  3. package/dist/adt/cds-impact.d.ts.map +1 -0
  4. package/dist/adt/cds-impact.js +91 -0
  5. package/dist/adt/cds-impact.js.map +1 -0
  6. package/dist/adt/client.d.ts +17 -1
  7. package/dist/adt/client.d.ts.map +1 -1
  8. package/dist/adt/client.js +91 -1
  9. package/dist/adt/client.js.map +1 -1
  10. package/dist/adt/codeintel.d.ts +8 -0
  11. package/dist/adt/codeintel.d.ts.map +1 -1
  12. package/dist/adt/codeintel.js +33 -0
  13. package/dist/adt/codeintel.js.map +1 -1
  14. package/dist/adt/config.d.ts +2 -0
  15. package/dist/adt/config.d.ts.map +1 -1
  16. package/dist/adt/config.js.map +1 -1
  17. package/dist/adt/cookies.d.ts +5 -0
  18. package/dist/adt/cookies.d.ts.map +1 -1
  19. package/dist/adt/cookies.js +14 -0
  20. package/dist/adt/cookies.js.map +1 -1
  21. package/dist/adt/crud.d.ts.map +1 -1
  22. package/dist/adt/crud.js +7 -0
  23. package/dist/adt/crud.js.map +1 -1
  24. package/dist/adt/ddic-xml.d.ts +16 -0
  25. package/dist/adt/ddic-xml.d.ts.map +1 -1
  26. package/dist/adt/ddic-xml.js +79 -0
  27. package/dist/adt/ddic-xml.js.map +1 -1
  28. package/dist/adt/devtools.d.ts +11 -0
  29. package/dist/adt/devtools.d.ts.map +1 -1
  30. package/dist/adt/devtools.js +33 -0
  31. package/dist/adt/devtools.js.map +1 -1
  32. package/dist/adt/http.d.ts +2 -0
  33. package/dist/adt/http.d.ts.map +1 -1
  34. package/dist/adt/http.js +54 -5
  35. package/dist/adt/http.js.map +1 -1
  36. package/dist/adt/refactoring.d.ts +60 -0
  37. package/dist/adt/refactoring.d.ts.map +1 -0
  38. package/dist/adt/refactoring.js +117 -0
  39. package/dist/adt/refactoring.js.map +1 -0
  40. package/dist/adt/transport.d.ts +26 -0
  41. package/dist/adt/transport.d.ts.map +1 -1
  42. package/dist/adt/transport.js +60 -0
  43. package/dist/adt/transport.js.map +1 -1
  44. package/dist/adt/types.d.ts +89 -0
  45. package/dist/adt/types.d.ts.map +1 -1
  46. package/dist/adt/xml-parser.d.ts +19 -1
  47. package/dist/adt/xml-parser.d.ts.map +1 -1
  48. package/dist/adt/xml-parser.js +173 -0
  49. package/dist/adt/xml-parser.js.map +1 -1
  50. package/dist/cli.js +5 -0
  51. package/dist/cli.js.map +1 -1
  52. package/dist/handlers/intent.d.ts.map +1 -1
  53. package/dist/handlers/intent.js +352 -15
  54. package/dist/handlers/intent.js.map +1 -1
  55. package/dist/handlers/schemas.d.ts +75 -35
  56. package/dist/handlers/schemas.d.ts.map +1 -1
  57. package/dist/handlers/schemas.js +68 -22
  58. package/dist/handlers/schemas.js.map +1 -1
  59. package/dist/handlers/tools.d.ts.map +1 -1
  60. package/dist/handlers/tools.js +122 -47
  61. package/dist/handlers/tools.js.map +1 -1
  62. package/dist/server/config.d.ts.map +1 -1
  63. package/dist/server/config.js +18 -0
  64. package/dist/server/config.js.map +1 -1
  65. package/dist/server/server.d.ts +7 -1
  66. package/dist/server/server.d.ts.map +1 -1
  67. package/dist/server/server.js +62 -6
  68. package/dist/server/server.js.map +1 -1
  69. package/dist/server/types.d.ts +4 -0
  70. package/dist/server/types.d.ts.map +1 -1
  71. package/dist/server/types.js +2 -0
  72. package/dist/server/types.js.map +1 -1
  73. package/package.json +8 -4
@@ -9,19 +9,21 @@
9
9
  * responses. Internal details (stack traces, SAP XML) are NOT
10
10
  * leaked to the LLM — only user-friendly error messages.
11
11
  */
12
+ import { classifyCdsImpact } from '../adt/cds-impact.js';
12
13
  import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '../adt/codeintel.js';
13
14
  import { createObject, deleteObject, lockObject, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, } from '../adt/crud.js';
14
- import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildPackageXml, buildServiceBindingXml, } from '../adt/ddic-xml.js';
15
- import { activate, activateBatch, applyFixProposal, getFixProposals, publishServiceBinding, runAtcCheck, runUnitTests, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
15
+ import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildPackageXml, buildServiceBindingXml, decodeKtdText, rewriteKtdText, } from '../adt/ddic-xml.js';
16
+ import { activate, activateBatch, applyFixProposal, getFixProposals, getPrettyPrinterSettings, prettyPrint, publishServiceBinding, runAtcCheck, runUnitTests, setPrettyPrinterSettings, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
16
17
  import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
17
18
  import { AdtApiError, AdtNetworkError, AdtSafetyError, classifySapDomainError, isNotFoundError, } from '../adt/errors.js';
18
19
  import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
19
20
  import { addTileToGroup, createCatalog, createGroup, createTile, deleteCatalog, listCatalogs, listGroups, listTiles, } from '../adt/flp.js';
21
+ import { changePackage } from '../adt/refactoring.js';
20
22
  import { checkOperation, checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
21
- import { createTransport, deleteTransport, getTransport, getTransportInfo, listTransports, reassignTransport, releaseTransport, releaseTransportRecursive, } from '../adt/transport.js';
23
+ import { createTransport, deleteTransport, getObjectTransports, getTransport, getTransportInfo, listTransports, reassignTransport, releaseTransport, releaseTransportRecursive, } from '../adt/transport.js';
22
24
  import { getAppInfo } from '../adt/ui5-repository.js';
23
25
  import { validateAffHeader } from '../aff/validator.js';
24
- import { extractCdsElements } from '../context/cds-deps.js';
26
+ import { extractCdsDependencies, extractCdsElements } from '../context/cds-deps.js';
25
27
  import { compressCdsContext, compressContext } from '../context/compressor.js';
26
28
  import { extractMethod, formatMethodListing, listMethods, spliceMethod } from '../context/method-surgery.js';
27
29
  import { buildLintConfig, listRulesFromConfig, } from '../lint/config-builder.js';
@@ -591,6 +593,21 @@ async function handleSAPRead(client, args, cachingLayer) {
591
593
  const { source, cacheHit } = await cachedGet('SRVB', name, () => client.getSrvb(name));
592
594
  return cachedTextResult(source, cacheHit);
593
595
  }
596
+ case 'SKTD': {
597
+ try {
598
+ // ADT returns a <sktd:docu> XML envelope with the Markdown body base64-encoded
599
+ // inside <sktd:text>. Cache the raw envelope (update flow re-uses it) and
600
+ // return the decoded Markdown to the LLM.
601
+ const { source, cacheHit } = await cachedGet('SKTD', name, () => client.getKtd(name));
602
+ return cachedTextResult(decodeKtdText(source), cacheHit);
603
+ }
604
+ catch (err) {
605
+ if (isNotFoundError(err)) {
606
+ return textResult(`No Knowledge Transfer Document (SKTD) found for "${name}". KTD docs are optional Markdown documentation attached to ABAP objects — either one was never created for "${name}", or the name is wrong.`);
607
+ }
608
+ throw err;
609
+ }
610
+ }
594
611
  case 'TABL': {
595
612
  const { source, cacheHit } = await cachedGet('TABL', name, () => client.getTable(name));
596
613
  return cachedTextResult(source, cacheHit);
@@ -611,6 +628,58 @@ async function handleSAPRead(client, args, cachingLayer) {
611
628
  const dtel = await client.getDataElement(name);
612
629
  return textResult(JSON.stringify(dtel, null, 2));
613
630
  }
631
+ case 'AUTH': {
632
+ const authField = await client.getAuthorizationField(name);
633
+ return textResult(JSON.stringify(authField, null, 2));
634
+ }
635
+ case 'FTG2': {
636
+ const toggle = await client.getFeatureToggle(name);
637
+ return textResult(JSON.stringify(toggle, null, 2));
638
+ }
639
+ case 'ENHO': {
640
+ const enhancement = await client.getEnhancementImplementation(name);
641
+ return textResult(JSON.stringify(enhancement, null, 2));
642
+ }
643
+ case 'VERSIONS': {
644
+ const include = typeof args.include === 'string' ? args.include : undefined;
645
+ let group = typeof args.group === 'string' ? args.group : undefined;
646
+ const objectType = normalizeObjectType(String(args.objectType ?? '')) || inferObjectType(name) || 'PROG';
647
+ if (objectType === 'FUNC' && !group) {
648
+ const resolved = cachingLayer
649
+ ? await cachingLayer.resolveFuncGroup(client, name)
650
+ : await client.resolveFunctionGroup(name);
651
+ if (!resolved) {
652
+ return errorResult(`Cannot resolve function group for "${name}". Provide the group parameter explicitly, or use SAPSearch("${name}") to find the function group.`);
653
+ }
654
+ group = resolved;
655
+ }
656
+ try {
657
+ const revisions = await client.getRevisions(objectType, name, { include, group });
658
+ return textResult(JSON.stringify(revisions, null, 2));
659
+ }
660
+ catch (err) {
661
+ if (isNotFoundError(err)) {
662
+ return textResult(`No version history available for ${objectType} "${name}" on this SAP system. ` +
663
+ `This usually means the object does not exist, or the ADT versions endpoint is not supported for ${objectType} on this backend release.`);
664
+ }
665
+ throw err;
666
+ }
667
+ }
668
+ case 'VERSION_SOURCE': {
669
+ const versionUri = String(args.versionUri ?? '');
670
+ if (!versionUri) {
671
+ return errorResult('VERSION_SOURCE requires a versionUri parameter. Get it from SAPRead(type="VERSIONS", name="...") response (.revisions[].uri).');
672
+ }
673
+ try {
674
+ return textResult(await client.getRevisionSource(versionUri));
675
+ }
676
+ catch (err) {
677
+ if (isNotFoundError(err)) {
678
+ return errorResult(`Revision at URI "${versionUri}" was not found. The revision may have been removed, or the URI is malformed. Fetch a fresh list via SAPRead(type="VERSIONS", name="...").`);
679
+ }
680
+ throw err;
681
+ }
682
+ }
614
683
  case 'TRAN': {
615
684
  const tran = await client.getTransaction(name);
616
685
  // Enrich with program name via SQL — only if free SQL is allowed by safety config
@@ -747,7 +816,7 @@ async function handleSAPRead(client, args, cachingLayer) {
747
816
  }
748
817
  }
749
818
  default:
750
- return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DCLS, 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. ` +
819
+ return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DCLS, DDLX, BDEF, SRVD, SRVB, SKTD, TABL, VIEW, STRU, DOMA, DTEL, AUTH, FTG2, ENHO, VERSIONS, VERSION_SOURCE, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS, BSP, BSP_DEPLOY, API_STATE, INACTIVE_OBJECTS. ` +
751
820
  'Tip: Type aliases are auto-normalized (e.g., DDLS/DF → DDLS, DCLS/DL → DCLS, CLAS/OC → CLAS, PROG/P → PROG). ' +
752
821
  'Do not pass a URI — use the "type" and "name" parameters instead.');
753
822
  }
@@ -835,9 +904,8 @@ async function handleSAPQuery(client, args) {
835
904
  throw err;
836
905
  }
837
906
  }
838
- // _client unused: SAPLint runs offline via @abaplint/core (no SAP round-trip).
839
- // Signature matches other handlers for consistency with handleToolCall dispatch.
840
- async function handleSAPLint(_client, args, config) {
907
+ // Some SAPLint actions run offline (@abaplint/core), others call SAP ADT formatter APIs.
908
+ async function handleSAPLint(client, args, config) {
841
909
  const action = String(args.action ?? '');
842
910
  const ruleOverrides = args.rules;
843
911
  const configOptions = buildLintConfigOptions(config, ruleOverrides);
@@ -876,8 +944,33 @@ async function handleSAPLint(_client, args, config) {
876
944
  disabledRuleNames: disabled.map((r) => r.rule),
877
945
  }, null, 2));
878
946
  }
947
+ case 'format': {
948
+ const source = String(args.source ?? '');
949
+ if (!source)
950
+ return errorResult('"source" is required for format action.');
951
+ const formatted = await prettyPrint(client.http, client.safety, source);
952
+ return textResult(formatted);
953
+ }
954
+ case 'get_formatter_settings': {
955
+ const settings = await getPrettyPrinterSettings(client.http, client.safety);
956
+ return textResult(JSON.stringify(settings, null, 2));
957
+ }
958
+ case 'set_formatter_settings': {
959
+ const indentation = args.indentation;
960
+ const style = args.style;
961
+ if (indentation === undefined && style === undefined) {
962
+ return errorResult('At least one of "indentation" or "style" is required for set_formatter_settings.');
963
+ }
964
+ const current = await getPrettyPrinterSettings(client.http, client.safety);
965
+ const next = {
966
+ indentation: indentation ?? current.indentation,
967
+ style: style ?? current.style,
968
+ };
969
+ await setPrettyPrinterSettings(client.http, client.safety, next);
970
+ return textResult(JSON.stringify(next, null, 2));
971
+ }
879
972
  default:
880
- return errorResult(`Unknown SAPLint action: "${action}". Supported: lint, lint_and_fix, list_rules. For atc/syntax/unittest, use SAPDiagnose instead.`);
973
+ return errorResult(`Unknown SAPLint action: "${action}". Supported: lint, lint_and_fix, list_rules, format, get_formatter_settings, set_formatter_settings. For atc/syntax/unittest, use SAPDiagnose instead.`);
881
974
  }
882
975
  }
883
976
  /**
@@ -904,12 +997,13 @@ const DATAELEMENT_V2_CONTENT_TYPE = 'application/vnd.sap.adt.dataelements.v2+xml
904
997
  const SERVICEBINDING_V2_CONTENT_TYPE = 'application/vnd.sap.adt.businessservices.servicebinding.v2+xml; charset=utf-8';
905
998
  const BDEF_CONTENT_TYPE = 'application/vnd.sap.adt.blues.v1+xml';
906
999
  const MESSAGECLASS_CONTENT_TYPE = 'application/vnd.sap.adt.mc.messageclass+xml';
1000
+ const SKTD_V2_CONTENT_TYPE = 'application/vnd.sap.adt.sktdv2+xml';
907
1001
  function isMetadataWriteType(type) {
908
1002
  return type === 'DOMA' || type === 'DTEL' || type === 'MSAG' || type === 'SRVB';
909
1003
  }
910
1004
  /** Types that require a specific vendor content type for creation (not application/*) */
911
1005
  function needsVendorContentType(type) {
912
- return type === 'DOMA' || type === 'DTEL' || type === 'BDEF' || type === 'MSAG';
1006
+ return type === 'DOMA' || type === 'DTEL' || type === 'BDEF' || type === 'MSAG' || type === 'SKTD';
913
1007
  }
914
1008
  /** Content type used for create POST */
915
1009
  function createContentTypeForType(type) {
@@ -946,6 +1040,8 @@ function vendorContentTypeForType(type) {
946
1040
  return BDEF_CONTENT_TYPE;
947
1041
  case 'MSAG':
948
1042
  return MESSAGECLASS_CONTENT_TYPE;
1043
+ case 'SKTD':
1044
+ return SKTD_V2_CONTENT_TYPE;
949
1045
  default:
950
1046
  // Wildcard lets the SAP server resolve the correct handler.
951
1047
  // Sending 'application/xml' causes 415 on DDL-based endpoints
@@ -1435,6 +1531,7 @@ const SLASH_TYPE_MAP = {
1435
1531
  'DEVC/K': 'DEVC',
1436
1532
  'TRAN/O': 'TRAN',
1437
1533
  'VIEW/V': 'VIEW',
1534
+ 'SKTD/TYP': 'SKTD',
1438
1535
  };
1439
1536
  /** Normalize ADT type codes and aliases to ARC-1 canonical short types. */
1440
1537
  export function normalizeObjectType(type) {
@@ -1545,13 +1642,17 @@ function objectBasePath(type) {
1545
1642
  return '/sap/bc/adt/packages/';
1546
1643
  case 'TRAN':
1547
1644
  return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
1645
+ case 'SKTD':
1646
+ return '/sap/bc/adt/documentation/ktd/documents/';
1548
1647
  default:
1549
1648
  return '/sap/bc/adt/programs/programs/';
1550
1649
  }
1551
1650
  }
1552
1651
  /** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. Name is URI-encoded. */
1553
1652
  function objectUrlForType(type, name) {
1554
- return `${objectBasePath(type)}${encodeURIComponent(name)}`;
1653
+ // KTD endpoints require lowercase object names in the URL path (confirmed via Eclipse ADT trace).
1654
+ const effectiveName = type === 'SKTD' ? name.toLowerCase() : name;
1655
+ return `${objectBasePath(type)}${encodeURIComponent(effectiveName)}`;
1555
1656
  }
1556
1657
  /** Infer SAP object type from naming conventions. Returns empty string if type cannot be determined. */
1557
1658
  function inferObjectType(name) {
@@ -1569,7 +1670,8 @@ function inferObjectType(name) {
1569
1670
  * Used for API release state where the full URI is encoded as a single path segment by the caller.
1570
1671
  */
1571
1672
  function objectUrlForTypeRaw(type, name) {
1572
- return `${objectBasePath(type)}${name}`;
1673
+ const effectiveName = type === 'SKTD' ? name.toLowerCase() : name;
1674
+ return `${objectBasePath(type)}${effectiveName}`;
1573
1675
  }
1574
1676
  /** Get the source URL for an object (appends /source/main) */
1575
1677
  function sourceUrlForType(type, name) {
@@ -1602,6 +1704,19 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1602
1704
  switch (action) {
1603
1705
  case 'update': {
1604
1706
  const existingPackage = await enforcePackageForExistingObject();
1707
+ if (type === 'SKTD') {
1708
+ // KTD update requires the full <sktd:docu> XML envelope with the Markdown
1709
+ // body base64-encoded inside <sktd:text>, PUT with
1710
+ // `application/vnd.sap.adt.sktdv2+xml`. PUTting raw text/plain silently
1711
+ // no-ops (or 415s on strict systems). Fetch the current envelope,
1712
+ // replace only the <sktd:text> body, and PUT it back — preserves
1713
+ // responsible/masterLanguage/packageRef/refObject metadata.
1714
+ const currentEnvelope = await client.getKtd(name);
1715
+ const body = rewriteKtdText(currentEnvelope, source);
1716
+ await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, transport);
1717
+ cachingLayer?.invalidate(type, name);
1718
+ return textResult(`Successfully updated ${type} ${name}.`);
1719
+ }
1605
1720
  if (isMetadataWriteType(type)) {
1606
1721
  // Metadata updates are full-XML-replace — we must fetch existing metadata
1607
1722
  // and merge with provided fields so omitted fields keep their current values.
@@ -1675,6 +1790,45 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1675
1790
  if (!affResult.valid) {
1676
1791
  return errorResult(`AFF metadata validation failed for ${type} ${name}:\n- ${(affResult.errors ?? []).join('\n- ')}\n\nFix the metadata and retry.`);
1677
1792
  }
1793
+ if (type === 'SKTD') {
1794
+ // A KTD is not a standalone object — it documents a parent object (e.g., a DDLS view or a CLAS).
1795
+ // The create POST goes to the collection URL with a sktd:docu XML body that references the parent.
1796
+ const refType = String(args.refObjectType ?? '');
1797
+ if (!refType) {
1798
+ return errorResult('"refObjectType" is required for SKTD create — the ADT type+subtype of the parent object being documented (e.g., "DDLS/DF", "CLAS/OC", "PROG/P", "INTF/OI", "BDEF/BDO", "SRVD/SRV").');
1799
+ }
1800
+ const refName = String(args.refObjectName ?? name);
1801
+ // SAP rule: a KTD's own name must equal the parent object's name (one KTD per object).
1802
+ // Creating a KTD named differently from its parent fails server-side with a cryptic
1803
+ // "Check of condition failed" — fail fast with a clear message instead.
1804
+ if (refName.toUpperCase() !== name.toUpperCase()) {
1805
+ return errorResult(`SKTD name "${name}" must match refObjectName "${refName}" — a Knowledge Transfer Document inherits the name of the ABAP object it documents (one KTD per object). To document "${refName}", call SAPWrite(action="create", type="SKTD", name="${refName}", refObjectType="${refType}", ...).`);
1806
+ }
1807
+ const refDescription = String(args.refObjectDescription ?? '');
1808
+ // Build the parent URI. ADT URIs use lowercase names by convention (matches the Eclipse trace).
1809
+ const refParentType = refType.split('/')[0] ?? '';
1810
+ const refUri = `${objectBasePath(refParentType)}${encodeURIComponent(refName.toLowerCase())}`;
1811
+ const ktdBody = `<?xml version="1.0" encoding="UTF-8"?>
1812
+ <sktd:docu xmlns:sktd="http://www.sap.com/wbobj/texts/sktd" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:language="EN" adtcore:name="${escapeXml(name)}" adtcore:type="SKTD/TYP" adtcore:masterLanguage="EN">
1813
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
1814
+ <sktd:refObject adtcore:description="${escapeXml(refDescription)}" adtcore:name="${escapeXml(refName)}" adtcore:type="${escapeXml(refType)}" adtcore:uri="${escapeXml(refUri)}"/>
1815
+ </sktd:docu>`;
1816
+ const ktdCreateUrl = '/sap/bc/adt/documentation/ktd/documents';
1817
+ const ktdResult = await createObject(client.http, client.safety, ktdCreateUrl, ktdBody, SKTD_V2_CONTENT_TYPE, effectiveTransport);
1818
+ // If initial Markdown was provided, follow up with an update PUT to write it.
1819
+ // Same envelope contract as the update path: fetch-then-rewrite ensures we
1820
+ // PUT back exactly the shape SAP gave us (with all the server-assigned
1821
+ // metadata), only swapping <sktd:text>.
1822
+ if (source) {
1823
+ const currentEnvelope = await client.getKtd(name);
1824
+ const body = rewriteKtdText(currentEnvelope, source);
1825
+ await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, effectiveTransport);
1826
+ cachingLayer?.invalidate(type, name);
1827
+ return textResult(`Created SKTD ${name} in package ${pkg} and wrote Markdown content.\nNext step: SAPActivate(type="SKTD", name="${name}").\n${ktdResult}`);
1828
+ }
1829
+ cachingLayer?.invalidate(type, name);
1830
+ return textResult(`Created SKTD ${name} in package ${pkg} (no Markdown content written — pass "source" to write the body).\nNext step: SAPActivate(type="SKTD", name="${name}").\n${ktdResult}`);
1831
+ }
1678
1832
  // Build type-specific creation XML body.
1679
1833
  // SAP ADT requires the root element to match the object type —
1680
1834
  // a generic objectReferences body returns 400 "System expected the element ...".
@@ -1785,7 +1939,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
1785
1939
  }
1786
1940
  case 'delete': {
1787
1941
  await enforcePackageForExistingObject();
1788
- // Lock, delete, unlock pattern — auto-propagate lock corrNr if no explicit transport
1942
+ // Lock, delete, unlock pattern (works for all types including SKTD) — auto-propagate lock corrNr if no explicit transport
1789
1943
  await client.http.withStatefulSession(async (session) => {
1790
1944
  const lock = await lockObject(session, client.safety, objectUrl);
1791
1945
  const effectiveTransport = transport ?? (lock.corrNr || undefined);
@@ -2504,14 +2658,56 @@ async function handleSAPTransport(client, args) {
2504
2658
  summary,
2505
2659
  }, null, 2));
2506
2660
  }
2661
+ case 'history': {
2662
+ const objectType = String(args.type ?? '');
2663
+ const objectName = String(args.name ?? '');
2664
+ if (!objectType || !objectName) {
2665
+ return errorResult('"type" and "name" are required for "history" action.');
2666
+ }
2667
+ const objectUrl = objectUrlForType(objectType, objectName);
2668
+ const primary = await getObjectTransports(client.http, client.safety, objectUrl);
2669
+ let candidateTransports = primary.candidateTransports;
2670
+ // Fallback: if per-object transport lookup is empty, derive the package via
2671
+ // the object metadata endpoint and ask transportchecks for candidate transports.
2672
+ if (primary.relatedTransports.length === 0 && candidateTransports.length === 0) {
2673
+ try {
2674
+ const pkg = await client.resolveObjectPackage(objectUrl);
2675
+ if (pkg && pkg !== '$TMP') {
2676
+ const info = await getTransportInfo(client.http, client.safety, objectUrl, pkg, '');
2677
+ candidateTransports = info.existingTransports;
2678
+ }
2679
+ }
2680
+ catch {
2681
+ // best-effort-fallback
2682
+ }
2683
+ }
2684
+ const lockOwner = primary.relatedTransports[0]?.owner;
2685
+ const summary = primary.lockedTransport
2686
+ ? `Object ${objectName} is locked in transport ${primary.lockedTransport}${lockOwner ? ` by ${lockOwner}` : ''}.`
2687
+ : candidateTransports.length > 0
2688
+ ? `Object ${objectName} has no active lock; ${candidateTransports.length} transport(s) available for assignment.`
2689
+ : `Object ${objectName} has no related or candidate transports (likely $TMP / local object).`;
2690
+ const history = {
2691
+ object: { type: objectType, name: objectName, uri: objectUrl },
2692
+ ...(primary.lockedTransport ? { lockedTransport: primary.lockedTransport } : {}),
2693
+ relatedTransports: primary.relatedTransports,
2694
+ candidateTransports,
2695
+ summary,
2696
+ };
2697
+ return textResult(JSON.stringify(history, null, 2));
2698
+ }
2507
2699
  default:
2508
- return errorResult(`Unknown SAPTransport action: ${action}. Supported: list, get, create, release, delete, reassign, release_recursive, check`);
2700
+ return errorResult(`Unknown SAPTransport action: ${action}. Supported: list, get, create, release, delete, reassign, release_recursive, check, history`);
2509
2701
  }
2510
2702
  }
2511
2703
  // ─── SAPContext Handler ───────────────────────────────────────────────
2512
2704
  async function handleSAPContext(client, args, cachingLayer) {
2513
2705
  const action = String(args.action ?? '');
2514
- const type = normalizeObjectType(String(args.type ?? ''));
2706
+ // action="impact" is DDLS-only on the server side — default the type so LLMs
2707
+ // don't have to supply it redundantly (and don't get a validation retry when
2708
+ // they don't). Any non-DDLS value still fails the guardrail below.
2709
+ const rawType = String(args.type ?? '');
2710
+ const type = normalizeObjectType(rawType || (action === 'impact' ? 'DDLS' : ''));
2515
2711
  const name = String(args.name ?? '');
2516
2712
  const maxDeps = Number(args.maxDeps ?? 20);
2517
2713
  const depth = Math.min(Math.max(Number(args.depth ?? 1), 1), 3);
@@ -2547,6 +2743,42 @@ async function handleSAPContext(client, args, cachingLayer) {
2547
2743
  const { source } = await cachingLayer.getSource(objType, objName, fetcher);
2548
2744
  return source;
2549
2745
  };
2746
+ if (action === 'impact') {
2747
+ if (type !== 'DDLS') {
2748
+ return errorResult('SAPContext(action="impact") supports DDLS only. For non-CDS objects, use SAPNavigate(action="references").');
2749
+ }
2750
+ const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
2751
+ const upstream = buildCdsUpstream(extractCdsDependencies(ddlSource));
2752
+ const includeIndirect = args.includeIndirect === true;
2753
+ let downstream = classifyCdsImpact([], { includeIndirect });
2754
+ const warnings = [];
2755
+ try {
2756
+ const whereUsed = await findWhereUsed(client.http, client.safety, objectUrlForType('DDLS', name));
2757
+ downstream = classifyCdsImpact(whereUsed, { includeIndirect });
2758
+ }
2759
+ catch (err) {
2760
+ if (err instanceof AdtApiError && [404, 405, 415, 501].includes(err.statusCode)) {
2761
+ warnings.push('Where-used endpoint not available on this system');
2762
+ }
2763
+ else {
2764
+ throw err;
2765
+ }
2766
+ }
2767
+ const upstreamCount = upstream.tables.length + upstream.views.length + upstream.associations.length + upstream.compositions.length;
2768
+ const response = {
2769
+ name,
2770
+ type: 'DDLS',
2771
+ upstream,
2772
+ downstream,
2773
+ summary: {
2774
+ upstreamCount,
2775
+ downstreamTotal: downstream.summary.total,
2776
+ downstreamDirect: downstream.summary.direct,
2777
+ },
2778
+ ...(warnings.length > 0 ? { warnings } : {}),
2779
+ };
2780
+ return textResult(JSON.stringify(response, null, 2));
2781
+ }
2550
2782
  // Get source — either provided or fetched from SAP
2551
2783
  let source;
2552
2784
  if (args.source) {
@@ -2615,6 +2847,45 @@ async function handleSAPContext(client, args, cachingLayer) {
2615
2847
  const result = await compressContext(client, source, name, type, maxDeps, depth, abaplintVersion, cachingLayer);
2616
2848
  return textResult(result.output);
2617
2849
  }
2850
+ function buildCdsUpstream(deps) {
2851
+ const tableNames = new Set();
2852
+ const viewNames = new Set();
2853
+ const associationNames = new Set();
2854
+ const compositionNames = new Set();
2855
+ for (const dep of deps) {
2856
+ const upperName = dep.name.toUpperCase();
2857
+ if (dep.kind === 'association') {
2858
+ associationNames.add(upperName);
2859
+ continue;
2860
+ }
2861
+ if (dep.kind === 'composition') {
2862
+ compositionNames.add(upperName);
2863
+ continue;
2864
+ }
2865
+ if (dep.kind === 'projection_base') {
2866
+ viewNames.add(upperName);
2867
+ continue;
2868
+ }
2869
+ if (isLikelyCdsViewName(upperName)) {
2870
+ viewNames.add(upperName);
2871
+ }
2872
+ else {
2873
+ tableNames.add(upperName);
2874
+ }
2875
+ }
2876
+ return {
2877
+ tables: [...tableNames].sort().map((name) => ({ name })),
2878
+ views: [...viewNames].sort().map((name) => ({ name })),
2879
+ associations: [...associationNames].sort().map((name) => ({ name })),
2880
+ compositions: [...compositionNames].sort().map((name) => ({ name })),
2881
+ };
2882
+ }
2883
+ function isLikelyCdsViewName(name) {
2884
+ if (name.startsWith('/')) {
2885
+ return /\/[ICRPAZ][A-Z0-9_]*_/.test(name);
2886
+ }
2887
+ return /^(ZI_|ZC_|ZR_|ZP_|I_|C_|R_|P_)/.test(name);
2888
+ }
2618
2889
  // ─── SAPManage Handler ────────────────────────────────────────────────
2619
2890
  /** Cached feature status — populated on first probe */
2620
2891
  let cachedFeatures;
@@ -2713,6 +2984,72 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
2713
2984
  });
2714
2985
  return textResult(`Deleted package ${name}.`);
2715
2986
  }
2987
+ case 'change_package': {
2988
+ const objectName = String(args.objectName ?? '').trim();
2989
+ const objectType = String(args.objectType ?? '').trim();
2990
+ const oldPackage = String(args.oldPackage ?? '').trim();
2991
+ const newPackage = String(args.newPackage ?? '').trim();
2992
+ const transport = String(args.transport ?? '').trim();
2993
+ let objectUri = String(args.objectUri ?? '').trim();
2994
+ if (!objectName)
2995
+ return errorResult('"objectName" is required for change_package action.');
2996
+ if (!objectType)
2997
+ return errorResult('"objectType" is required for change_package action.');
2998
+ if (!oldPackage)
2999
+ return errorResult('"oldPackage" is required for change_package action.');
3000
+ if (!newPackage)
3001
+ return errorResult('"newPackage" is required for change_package action.');
3002
+ checkOperation(client.safety, OperationType.Update, 'ChangePackage');
3003
+ checkPackage(client.safety, oldPackage);
3004
+ checkPackage(client.safety, newPackage);
3005
+ // Resolve object URI via search if not provided
3006
+ if (!objectUri) {
3007
+ const searchResp = await client.http.get(`/sap/bc/adt/repository/informationsystem/search?operation=quickSearch&query=${encodeURIComponent(objectName)}&maxResults=10`);
3008
+ const uriMatch = searchResp.body.match(new RegExp(`adtcore:uri="([^"]*)"[^>]*adtcore:type="${objectType.replace('/', '\\/')}"`, 'i'));
3009
+ if (!uriMatch?.[1]) {
3010
+ return errorResult(`Could not find object "${objectName}" with type "${objectType}" via ADT search. ` +
3011
+ `Verify the object exists and the type is correct (e.g., CLAS/OC, DDLS/DF, PROG/P).`);
3012
+ }
3013
+ objectUri = uriMatch[1];
3014
+ }
3015
+ // Transport pre-flight for non-local target packages
3016
+ let effectiveTransport = transport || undefined;
3017
+ if (!effectiveTransport && newPackage.toUpperCase() !== '$TMP') {
3018
+ try {
3019
+ const transportInfo = await getTransportInfo(client.http, client.safety, objectUri, newPackage, 'I');
3020
+ if (transportInfo.lockedTransport) {
3021
+ effectiveTransport = transportInfo.lockedTransport;
3022
+ }
3023
+ else if (!transportInfo.isLocal && transportInfo.recording) {
3024
+ const existingList = transportInfo.existingTransports.length > 0
3025
+ ? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
3026
+ .slice(0, 10)
3027
+ .map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
3028
+ .join('\n')}`
3029
+ : '';
3030
+ return errorResult(`Package "${newPackage}" requires a transport number for change_package, but none was provided.\n\n` +
3031
+ `To fix this, either:\n` +
3032
+ `1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
3033
+ `2. Use SAPTransport(action="create", description="...") to create a new one\n` +
3034
+ `3. Then retry SAPManage(action="change_package", ..., transport="<transport_id>")` +
3035
+ existingList);
3036
+ }
3037
+ }
3038
+ catch {
3039
+ // Graceful fallback: let SAP enforce transport requirements if the pre-check fails.
3040
+ }
3041
+ }
3042
+ const result = await changePackage(client.http, client.safety, {
3043
+ objectUri,
3044
+ objectType,
3045
+ objectName,
3046
+ oldPackage,
3047
+ newPackage,
3048
+ transport: effectiveTransport,
3049
+ });
3050
+ const transportNote = result.transport ? ` (transport: ${result.transport})` : '';
3051
+ return textResult(`Moved ${objectName} from package ${oldPackage} to ${newPackage}${transportNote}.`);
3052
+ }
2716
3053
  case 'flp_list_catalogs': {
2717
3054
  const catalogs = await listCatalogs(client.http, client.safety);
2718
3055
  const customCount = catalogs.filter((c) => /^(Z|Y)/i.test(c.domainId)).length;