arc-1 0.2.0 → 0.4.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 (159) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +77 -199
  3. package/dist/adt/btp.d.ts +2 -2
  4. package/dist/adt/btp.d.ts.map +1 -1
  5. package/dist/adt/btp.js +1 -1
  6. package/dist/adt/btp.js.map +1 -1
  7. package/dist/adt/client.d.ts +13 -1
  8. package/dist/adt/client.d.ts.map +1 -1
  9. package/dist/adt/client.js +40 -1
  10. package/dist/adt/client.js.map +1 -1
  11. package/dist/adt/codeintel.d.ts +41 -0
  12. package/dist/adt/codeintel.d.ts.map +1 -1
  13. package/dist/adt/codeintel.js +99 -27
  14. package/dist/adt/codeintel.js.map +1 -1
  15. package/dist/adt/config.d.ts +6 -0
  16. package/dist/adt/config.d.ts.map +1 -1
  17. package/dist/adt/config.js.map +1 -1
  18. package/dist/adt/cookies.d.ts.map +1 -1
  19. package/dist/adt/cookies.js.map +1 -1
  20. package/dist/adt/crud.d.ts.map +1 -1
  21. package/dist/adt/crud.js.map +1 -1
  22. package/dist/adt/devtools.d.ts +14 -0
  23. package/dist/adt/devtools.d.ts.map +1 -1
  24. package/dist/adt/devtools.js +28 -3
  25. package/dist/adt/devtools.js.map +1 -1
  26. package/dist/adt/diagnostics.d.ts +101 -0
  27. package/dist/adt/diagnostics.d.ts.map +1 -0
  28. package/dist/adt/diagnostics.js +349 -0
  29. package/dist/adt/diagnostics.js.map +1 -0
  30. package/dist/adt/errors.d.ts.map +1 -1
  31. package/dist/adt/errors.js.map +1 -1
  32. package/dist/adt/features.d.ts +11 -2
  33. package/dist/adt/features.d.ts.map +1 -1
  34. package/dist/adt/features.js +37 -12
  35. package/dist/adt/features.js.map +1 -1
  36. package/dist/adt/http.d.ts +33 -7
  37. package/dist/adt/http.d.ts.map +1 -1
  38. package/dist/adt/http.js +148 -84
  39. package/dist/adt/http.js.map +1 -1
  40. package/dist/adt/oauth.d.ts +120 -0
  41. package/dist/adt/oauth.d.ts.map +1 -0
  42. package/dist/adt/oauth.js +321 -0
  43. package/dist/adt/oauth.js.map +1 -0
  44. package/dist/adt/safety.d.ts.map +1 -1
  45. package/dist/adt/safety.js.map +1 -1
  46. package/dist/adt/transport.d.ts.map +1 -1
  47. package/dist/adt/transport.js.map +1 -1
  48. package/dist/adt/types.d.ts +143 -0
  49. package/dist/adt/types.d.ts.map +1 -1
  50. package/dist/adt/types.js.map +1 -1
  51. package/dist/adt/xml-parser.d.ts +37 -1
  52. package/dist/adt/xml-parser.d.ts.map +1 -1
  53. package/dist/adt/xml-parser.js +147 -0
  54. package/dist/adt/xml-parser.js.map +1 -1
  55. package/dist/cache/cache.d.ts +47 -4
  56. package/dist/cache/cache.d.ts.map +1 -1
  57. package/dist/cache/cache.js +16 -5
  58. package/dist/cache/cache.js.map +1 -1
  59. package/dist/cache/caching-layer.d.ts +82 -0
  60. package/dist/cache/caching-layer.d.ts.map +1 -0
  61. package/dist/cache/caching-layer.js +134 -0
  62. package/dist/cache/caching-layer.js.map +1 -0
  63. package/dist/cache/memory.d.ts +14 -2
  64. package/dist/cache/memory.d.ts.map +1 -1
  65. package/dist/cache/memory.js +72 -5
  66. package/dist/cache/memory.js.map +1 -1
  67. package/dist/cache/sqlite.d.ts +10 -1
  68. package/dist/cache/sqlite.d.ts.map +1 -1
  69. package/dist/cache/sqlite.js +90 -2
  70. package/dist/cache/sqlite.js.map +1 -1
  71. package/dist/cache/warmup.d.ts +41 -0
  72. package/dist/cache/warmup.d.ts.map +1 -0
  73. package/dist/cache/warmup.js +286 -0
  74. package/dist/cache/warmup.js.map +1 -0
  75. package/dist/cli.d.ts.map +1 -1
  76. package/dist/cli.js.map +1 -1
  77. package/dist/context/cds-deps.d.ts +35 -0
  78. package/dist/context/cds-deps.d.ts.map +1 -0
  79. package/dist/context/cds-deps.js +201 -0
  80. package/dist/context/cds-deps.js.map +1 -0
  81. package/dist/context/compressor.d.ts +20 -1
  82. package/dist/context/compressor.d.ts.map +1 -1
  83. package/dist/context/compressor.js +178 -17
  84. package/dist/context/compressor.js.map +1 -1
  85. package/dist/context/contract.d.ts.map +1 -1
  86. package/dist/context/contract.js.map +1 -1
  87. package/dist/context/deps.d.ts.map +1 -1
  88. package/dist/context/deps.js.map +1 -1
  89. package/dist/context/method-surgery.d.ts +91 -0
  90. package/dist/context/method-surgery.d.ts.map +1 -0
  91. package/dist/context/method-surgery.js +441 -0
  92. package/dist/context/method-surgery.js.map +1 -0
  93. package/dist/context/types.d.ts +7 -0
  94. package/dist/context/types.d.ts.map +1 -1
  95. package/dist/context/types.js.map +1 -1
  96. package/dist/handlers/hyperfocused.d.ts +35 -0
  97. package/dist/handlers/hyperfocused.d.ts.map +1 -0
  98. package/dist/handlers/hyperfocused.js +104 -0
  99. package/dist/handlers/hyperfocused.js.map +1 -0
  100. package/dist/handlers/intent.d.ts +5 -1
  101. package/dist/handlers/intent.d.ts.map +1 -1
  102. package/dist/handlers/intent.js +462 -44
  103. package/dist/handlers/intent.js.map +1 -1
  104. package/dist/handlers/tools.d.ts +5 -0
  105. package/dist/handlers/tools.d.ts.map +1 -1
  106. package/dist/handlers/tools.js +270 -87
  107. package/dist/handlers/tools.js.map +1 -1
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js.map +1 -1
  110. package/dist/lint/config-builder.d.ts +60 -0
  111. package/dist/lint/config-builder.d.ts.map +1 -0
  112. package/dist/lint/config-builder.js +187 -0
  113. package/dist/lint/config-builder.js.map +1 -0
  114. package/dist/lint/lint.d.ts +43 -0
  115. package/dist/lint/lint.d.ts.map +1 -1
  116. package/dist/lint/lint.js +77 -2
  117. package/dist/lint/lint.js.map +1 -1
  118. package/dist/lint/presets/cloud.d.ts +16 -0
  119. package/dist/lint/presets/cloud.d.ts.map +1 -0
  120. package/dist/lint/presets/cloud.js +111 -0
  121. package/dist/lint/presets/cloud.js.map +1 -0
  122. package/dist/lint/presets/onprem.d.ts +17 -0
  123. package/dist/lint/presets/onprem.d.ts.map +1 -0
  124. package/dist/lint/presets/onprem.js +82 -0
  125. package/dist/lint/presets/onprem.js.map +1 -0
  126. package/dist/server/audit.d.ts +1 -0
  127. package/dist/server/audit.d.ts.map +1 -1
  128. package/dist/server/audit.js.map +1 -1
  129. package/dist/server/config.d.ts.map +1 -1
  130. package/dist/server/config.js +20 -0
  131. package/dist/server/config.js.map +1 -1
  132. package/dist/server/context.d.ts.map +1 -1
  133. package/dist/server/context.js.map +1 -1
  134. package/dist/server/elicit.d.ts.map +1 -1
  135. package/dist/server/elicit.js.map +1 -1
  136. package/dist/server/http.d.ts.map +1 -1
  137. package/dist/server/http.js +4 -1
  138. package/dist/server/http.js.map +1 -1
  139. package/dist/server/logger.d.ts.map +1 -1
  140. package/dist/server/logger.js.map +1 -1
  141. package/dist/server/server.d.ts +4 -2
  142. package/dist/server/server.d.ts.map +1 -1
  143. package/dist/server/server.js +121 -8
  144. package/dist/server/server.js.map +1 -1
  145. package/dist/server/sinks/btp-auditlog.d.ts.map +1 -1
  146. package/dist/server/sinks/btp-auditlog.js.map +1 -1
  147. package/dist/server/sinks/file.d.ts.map +1 -1
  148. package/dist/server/sinks/file.js.map +1 -1
  149. package/dist/server/sinks/stderr.d.ts.map +1 -1
  150. package/dist/server/sinks/stderr.js.map +1 -1
  151. package/dist/server/sinks/types.d.ts.map +1 -1
  152. package/dist/server/sinks/types.js.map +1 -1
  153. package/dist/server/types.d.ts +19 -0
  154. package/dist/server/types.d.ts.map +1 -1
  155. package/dist/server/types.js +8 -0
  156. package/dist/server/types.js.map +1 -1
  157. package/dist/server/xsuaa.d.ts.map +1 -1
  158. package/dist/server/xsuaa.js.map +1 -1
  159. package/package.json +14 -6
@@ -9,17 +9,23 @@
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 { findDefinition, findReferences, getCompletion } from '../adt/codeintel.js';
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, runAtcCheck, runUnitTests, syntaxCheck } from '../adt/devtools.js';
14
+ import { activate, activateBatch, runAtcCheck, runUnitTests, syntaxCheck } from '../adt/devtools.js';
15
+ import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
15
16
  import { AdtApiError, AdtNetworkError, AdtSafetyError } from '../adt/errors.js';
16
17
  import { mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
18
+ import { isOperationAllowed, OperationType } from '../adt/safety.js';
17
19
  import { createTransport, getTransport, listTransports, releaseTransport } from '../adt/transport.js';
18
- import { compressContext } from '../context/compressor.js';
19
- import { detectFilename, lintAbapSource } from '../lint/lint.js';
20
+ import { extractCdsElements } from '../context/cds-deps.js';
21
+ import { compressCdsContext, compressContext } from '../context/compressor.js';
22
+ import { extractMethod, formatMethodListing, listMethods, spliceMethod } from '../context/method-surgery.js';
23
+ import { buildLintConfig, listRulesFromConfig, } from '../lint/config-builder.js';
24
+ import { detectFilename, lintAbapSource, lintAndFix, validateBeforeWrite } from '../lint/lint.js';
20
25
  import { sanitizeArgs } from '../server/audit.js';
21
26
  import { generateRequestId, requestContext } from '../server/context.js';
22
27
  import { logger } from '../server/logger.js';
28
+ import { expandHyperfocusedArgs, getHyperfocusedScope } from './hyperfocused.js';
23
29
  /**
24
30
  * Scope required for each tool.
25
31
  *
@@ -86,7 +92,7 @@ function classifyError(err) {
86
92
  * all tools are allowed (backward compatibility).
87
93
  * @param server - MCP Server instance for elicitation support.
88
94
  */
89
- export async function handleToolCall(client, _config, toolName, args, authInfo, _server) {
95
+ export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer) {
90
96
  const reqId = generateRequestId();
91
97
  const start = Date.now();
92
98
  // Build user context for audit logging
@@ -127,7 +133,7 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
127
133
  let result;
128
134
  switch (toolName) {
129
135
  case 'SAPRead':
130
- result = await handleSAPRead(client, args);
136
+ result = await handleSAPRead(client, args, cachingLayer);
131
137
  break;
132
138
  case 'SAPSearch':
133
139
  result = await handleSAPSearch(client, args);
@@ -136,7 +142,7 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
136
142
  result = await handleSAPQuery(client, args);
137
143
  break;
138
144
  case 'SAPWrite':
139
- result = await handleSAPWrite(client, args);
145
+ result = await handleSAPWrite(client, args, config, cachingLayer);
140
146
  break;
141
147
  case 'SAPActivate':
142
148
  result = await handleSAPActivate(client, args);
@@ -145,7 +151,7 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
145
151
  result = await handleSAPNavigate(client, args);
146
152
  break;
147
153
  case 'SAPLint':
148
- result = await handleSAPLint(client, args);
154
+ result = await handleSAPLint(client, args, config);
149
155
  break;
150
156
  case 'SAPDiagnose':
151
157
  result = await handleSAPDiagnose(client, args);
@@ -154,11 +160,30 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
154
160
  result = await handleSAPTransport(client, args);
155
161
  break;
156
162
  case 'SAPContext':
157
- result = await handleSAPContext(client, args);
163
+ result = await handleSAPContext(client, args, cachingLayer);
158
164
  break;
159
165
  case 'SAPManage':
160
- result = await handleSAPManage(client, _config, args);
166
+ result = await handleSAPManage(client, config, args, cachingLayer);
161
167
  break;
168
+ case 'SAP': {
169
+ // Hyperfocused mode: route to the appropriate handler
170
+ const expanded = expandHyperfocusedArgs(args);
171
+ if ('error' in expanded) {
172
+ result = errorResult(expanded.error);
173
+ break;
174
+ }
175
+ // Check scope for the delegated action
176
+ if (authInfo) {
177
+ const requiredScope = getHyperfocusedScope(String(args.action ?? ''));
178
+ if (!authInfo.scopes.includes(requiredScope)) {
179
+ result = errorResult(`Insufficient scope: '${requiredScope}' required for SAP(action="${args.action}"). Your scopes: [${authInfo.scopes.join(', ')}]`);
180
+ break;
181
+ }
182
+ }
183
+ // Delegate to the real handler (recursive call, but with the mapped tool name)
184
+ result = await handleToolCall(client, config, expanded.toolName, expanded.expandedArgs, authInfo, _server, cachingLayer);
185
+ break;
186
+ }
162
187
  default:
163
188
  result = errorResult(`Unknown tool: ${toolName}`);
164
189
  }
@@ -168,7 +193,7 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
168
193
  const resultPreview = fullText.length > 500 ? `${fullText.slice(0, 500)}...` : fullText;
169
194
  logger.emitAudit({
170
195
  timestamp: new Date().toISOString(),
171
- level: 'info',
196
+ level: result.isError ? 'error' : 'info',
172
197
  event: 'tool_call_end',
173
198
  requestId: reqId,
174
199
  user,
@@ -177,6 +202,7 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
177
202
  durationMs,
178
203
  status: result.isError ? 'error' : 'success',
179
204
  errorMessage: result.isError ? result.content[0]?.text : undefined,
205
+ errorClass: result.isError ? 'result-path' : undefined,
180
206
  resultSize,
181
207
  resultPreview,
182
208
  });
@@ -203,26 +229,76 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
203
229
  });
204
230
  }
205
231
  // ─── Individual Tool Handlers ────────────────────────────────────────
206
- async function handleSAPRead(client, args) {
232
+ /** Check if the connected system is BTP ABAP Environment */
233
+ function isBtpSystem() {
234
+ return cachedFeatures?.systemType === 'btp';
235
+ }
236
+ /** BTP-specific error messages for unavailable operations */
237
+ const BTP_HINTS = {
238
+ PROG: 'Executable programs (reports) are not available on BTP ABAP Environment. Use CLAS with IF_OO_ADT_CLASSRUN for console applications.',
239
+ INCL: 'Includes are not available on BTP ABAP Environment. Use classes and interfaces instead — INCLUDE is forbidden in ABAP Cloud.',
240
+ VIEW: 'Classic DDIC views are not available on BTP ABAP Environment. Use DDLS (CDS views) instead.',
241
+ TEXT_ELEMENTS: 'Text elements are not available on BTP ABAP Environment (no classic programs). Use message classes or constant classes instead.',
242
+ VARIANTS: 'Variants are not available on BTP ABAP Environment (no classic programs).',
243
+ SOBJ: 'BOR business objects (SOBJ) are not available on BTP ABAP Environment. Use RAP behavior definitions (BDEF) instead.',
244
+ TRAN: 'Transaction codes (TRAN) are not available on BTP ABAP Environment. Use SAPSearch to find apps and services instead.',
245
+ };
246
+ async function handleSAPRead(client, args, cachingLayer) {
207
247
  const type = String(args.type ?? '');
208
248
  const name = String(args.name ?? '');
249
+ // BTP: return helpful error for unavailable types
250
+ if (isBtpSystem() && BTP_HINTS[type]) {
251
+ return errorResult(BTP_HINTS[type]);
252
+ }
253
+ // Helper: get source with cache support
254
+ const cachedGet = async (objType, objName, fetcher) => {
255
+ if (!cachingLayer)
256
+ return fetcher();
257
+ const { source } = await cachingLayer.getSource(objType, objName, fetcher);
258
+ return source;
259
+ };
209
260
  switch (type) {
210
261
  case 'PROG':
211
- return textResult(await client.getProgram(name));
212
- case 'CLAS':
262
+ return textResult(await cachedGet('PROG', name, () => client.getProgram(name)));
263
+ case 'CLAS': {
264
+ const methodParam = args.method;
265
+ if (methodParam && !args.include) {
266
+ // Method-level read — fetch full source then extract
267
+ const fullSource = await cachedGet('CLAS', name, () => client.getClass(name));
268
+ const abaplintVer = cachedFeatures?.abapRelease
269
+ ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
270
+ : undefined;
271
+ if (methodParam === '*') {
272
+ const listing = listMethods(fullSource, name, abaplintVer);
273
+ return textResult(formatMethodListing(listing));
274
+ }
275
+ const extracted = extractMethod(fullSource, name, methodParam, abaplintVer);
276
+ if (!extracted.success) {
277
+ return errorResult(extracted.error ?? `Method "${methodParam}" not found in ${name}.`);
278
+ }
279
+ return textResult(extracted.methodSource);
280
+ }
281
+ // Only cache the full merged source (no include param), not individual includes
282
+ if (!args.include) {
283
+ return textResult(await cachedGet('CLAS', name, () => client.getClass(name)));
284
+ }
213
285
  return textResult(await client.getClass(name, args.include));
286
+ }
214
287
  case 'INTF':
215
- return textResult(await client.getInterface(name));
288
+ return textResult(await cachedGet('INTF', name, () => client.getInterface(name)));
216
289
  case 'FUNC': {
217
290
  let group = String(args.group ?? '');
218
291
  if (!group) {
219
- const resolved = await client.resolveFunctionGroup(name);
292
+ // Use cached func group resolution if available
293
+ const resolved = cachingLayer
294
+ ? await cachingLayer.resolveFuncGroup(client, name)
295
+ : await client.resolveFunctionGroup(name);
220
296
  if (!resolved) {
221
297
  return errorResult(`Cannot resolve function group for "${name}". Provide the group parameter explicitly, or use SAPSearch("${name}") to find the function group.`);
222
298
  }
223
299
  group = resolved;
224
300
  }
225
- return textResult(await client.getFunction(group, name));
301
+ return textResult(await cachedGet('FUNC', name, () => client.getFunction(group, name)));
226
302
  }
227
303
  case 'FUGR': {
228
304
  const expand = Boolean(args.expand_includes);
@@ -248,17 +324,53 @@ async function handleSAPRead(client, args) {
248
324
  return textResult(JSON.stringify(fg, null, 2));
249
325
  }
250
326
  case 'INCL':
251
- return textResult(await client.getInclude(name));
252
- case 'DDLS':
253
- return textResult(await client.getDdls(name));
327
+ return textResult(await cachedGet('INCL', name, () => client.getInclude(name)));
328
+ case 'DDLS': {
329
+ const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
330
+ if (args.include?.toLowerCase() === 'elements') {
331
+ return textResult(extractCdsElements(ddlSource, name));
332
+ }
333
+ return textResult(ddlSource);
334
+ }
254
335
  case 'BDEF':
255
- return textResult(await client.getBdef(name));
336
+ return textResult(await cachedGet('BDEF', name, () => client.getBdef(name)));
256
337
  case 'SRVD':
257
- return textResult(await client.getSrvd(name));
338
+ return textResult(await cachedGet('SRVD', name, () => client.getSrvd(name)));
339
+ case 'DDLX':
340
+ return textResult(await cachedGet('DDLX', name, () => client.getDdlx(name)));
341
+ case 'SRVB':
342
+ return textResult(await cachedGet('SRVB', name, () => client.getSrvb(name)));
258
343
  case 'TABL':
259
- return textResult(await client.getTable(name));
344
+ return textResult(await cachedGet('TABL', name, () => client.getTable(name)));
260
345
  case 'VIEW':
261
- return textResult(await client.getView(name));
346
+ return textResult(await cachedGet('VIEW', name, () => client.getView(name)));
347
+ case 'STRU':
348
+ return textResult(await cachedGet('STRU', name, () => client.getStructure(name)));
349
+ case 'DOMA': {
350
+ const domain = await client.getDomain(name);
351
+ return textResult(JSON.stringify(domain, null, 2));
352
+ }
353
+ case 'DTEL': {
354
+ const dtel = await client.getDataElement(name);
355
+ return textResult(JSON.stringify(dtel, null, 2));
356
+ }
357
+ case 'TRAN': {
358
+ const tran = await client.getTransaction(name);
359
+ // Enrich with program name via SQL — only if free SQL is allowed by safety config
360
+ if (isOperationAllowed(client.safety, OperationType.FreeSQL)) {
361
+ try {
362
+ const safeName = name.toUpperCase().replace(/[^A-Z0-9_/]/g, '');
363
+ const data = await client.runQuery(`SELECT TCODE, PGMNA FROM TSTC WHERE TCODE = '${safeName}'`, 1);
364
+ if (data.rows.length > 0) {
365
+ tran.program = String(data.rows[0].PGMNA ?? '').trim();
366
+ }
367
+ }
368
+ catch {
369
+ // SQL failed (e.g., TSTC not found on BTP) — still return metadata
370
+ }
371
+ }
372
+ return textResult(JSON.stringify(tran, null, 2));
373
+ }
262
374
  case 'TABLE_CONTENTS': {
263
375
  const maxRows = Number(args.maxRows ?? 100);
264
376
  const data = await client.getTableContents(name, maxRows, args.sqlFilter);
@@ -309,7 +421,7 @@ async function handleSAPRead(client, args) {
309
421
  case 'VARIANTS':
310
422
  return textResult(await client.getVariants(name));
311
423
  default:
312
- return errorResult(`Unknown SAPRead type: ${type}. Supported: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, BDEF, SRVD, TABL, VIEW, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS`);
424
+ return errorResult(`Unknown SAPRead type: ${type}. Supported: 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`);
313
425
  }
314
426
  }
315
427
  async function handleSAPSearch(client, args) {
@@ -365,20 +477,69 @@ async function handleSAPQuery(client, args) {
365
477
  throw err;
366
478
  }
367
479
  }
368
- async function handleSAPLint(_client, args) {
480
+ // _client unused: SAPLint runs offline via @abaplint/core (no SAP round-trip).
481
+ // Signature matches other handlers for consistency with handleToolCall dispatch.
482
+ async function handleSAPLint(_client, args, config) {
369
483
  const action = String(args.action ?? '');
484
+ const ruleOverrides = args.rules;
485
+ const configOptions = buildLintConfigOptions(config, ruleOverrides);
370
486
  switch (action) {
371
487
  case 'lint': {
372
488
  const source = String(args.source ?? '');
489
+ if (!source)
490
+ return errorResult('"source" is required for lint action.');
373
491
  const name = String(args.name ?? 'UNKNOWN');
374
492
  const filename = detectFilename(source, name);
375
- const issues = lintAbapSource(source, filename);
493
+ const lintConfig = buildLintConfig(configOptions);
494
+ const issues = lintAbapSource(source, filename, lintConfig);
376
495
  return textResult(JSON.stringify(issues, null, 2));
377
496
  }
497
+ case 'lint_and_fix': {
498
+ const source = String(args.source ?? '');
499
+ if (!source)
500
+ return errorResult('"source" is required for lint_and_fix action.');
501
+ const name = String(args.name ?? 'UNKNOWN');
502
+ const filename = detectFilename(source, name);
503
+ const lintConfig = buildLintConfig(configOptions);
504
+ const result = lintAndFix(source, filename, lintConfig);
505
+ return textResult(JSON.stringify(result, null, 2));
506
+ }
507
+ case 'list_rules': {
508
+ const lintConfig = buildLintConfig(configOptions);
509
+ const rules = listRulesFromConfig(lintConfig);
510
+ const enabled = rules.filter((r) => r.enabled);
511
+ const disabled = rules.filter((r) => !r.enabled);
512
+ return textResult(JSON.stringify({
513
+ preset: configOptions.systemType === 'btp' ? 'cloud' : 'onprem',
514
+ abapVersion: cachedFeatures?.abapRelease ?? 'unknown',
515
+ enabledRules: enabled.length,
516
+ disabledRules: disabled.length,
517
+ rules: enabled,
518
+ disabledRuleNames: disabled.map((r) => r.rule),
519
+ }, null, 2));
520
+ }
378
521
  default:
379
- return errorResult(`Unknown SAPLint action: ${action}. Supported: lint, atc, syntax`);
522
+ return errorResult(`Unknown SAPLint action: "${action}". Supported: lint, lint_and_fix, list_rules. For atc/syntax/unittest, use SAPDiagnose instead.`);
380
523
  }
381
524
  }
525
+ /**
526
+ * Build LintConfigOptions from server config and cached features.
527
+ *
528
+ * Uses cachedFeatures (from SAPManage probe) when available, but falls back
529
+ * to config.systemType so that --system-type btp works even before the first
530
+ * probe. Without this fallback, cloud lint rules wouldn't apply until a probe
531
+ * populates cachedFeatures.
532
+ */
533
+ function buildLintConfigOptions(config, ruleOverrides) {
534
+ // Probe-detected system type is most accurate; fall back to CLI config
535
+ const systemType = cachedFeatures?.systemType ?? (config.systemType !== 'auto' ? config.systemType : undefined);
536
+ return {
537
+ systemType,
538
+ abapRelease: cachedFeatures?.abapRelease,
539
+ configFile: config.abaplintConfig,
540
+ ruleOverrides,
541
+ };
542
+ }
382
543
  // ─── Object URL Mapping ──────────────────────────────────────────────
383
544
  /** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. */
384
545
  function objectUrlForType(type, name) {
@@ -402,8 +563,20 @@ function objectUrlForType(type, name) {
402
563
  return `/sap/bc/adt/bo/behaviordefinitions/${encoded}`;
403
564
  case 'SRVD':
404
565
  return `/sap/bc/adt/ddic/srvd/sources/${encoded}`;
566
+ case 'DDLX':
567
+ return `/sap/bc/adt/ddic/ddlx/sources/${encoded}`;
568
+ case 'SRVB':
569
+ return `/sap/bc/adt/businessservices/bindings/${encoded}`;
405
570
  case 'TABL':
406
571
  return `/sap/bc/adt/ddic/tables/${encoded}`;
572
+ case 'STRU':
573
+ return `/sap/bc/adt/ddic/structures/${encoded}`;
574
+ case 'DOMA':
575
+ return `/sap/bc/adt/ddic/domains/${encoded}`;
576
+ case 'DTEL':
577
+ return `/sap/bc/adt/ddic/dataelements/${encoded}`;
578
+ case 'TRAN':
579
+ return `/sap/bc/adt/vit/wb/object_type/trant/object_name/${encoded}`;
407
580
  default:
408
581
  return `/sap/bc/adt/programs/programs/${encoded}`;
409
582
  }
@@ -413,7 +586,7 @@ function sourceUrlForType(type, name) {
413
586
  return `${objectUrlForType(type, name)}/source/main`;
414
587
  }
415
588
  // ─── SAPWrite Handler ────────────────────────────────────────────────
416
- async function handleSAPWrite(client, args) {
589
+ async function handleSAPWrite(client, args, config, cachingLayer) {
417
590
  const action = String(args.action ?? '');
418
591
  const type = String(args.type ?? '');
419
592
  const name = String(args.name ?? '');
@@ -423,8 +596,14 @@ async function handleSAPWrite(client, args) {
423
596
  const srcUrl = sourceUrlForType(type, name);
424
597
  switch (action) {
425
598
  case 'update': {
599
+ // Pre-write lint validation
600
+ const lintWarnings = runPreWriteLint(source, type, name, config);
601
+ if (lintWarnings.blocked)
602
+ return lintWarnings.result;
426
603
  await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport);
427
- return textResult(`Successfully updated ${type} ${name}.`);
604
+ cachingLayer?.invalidate(type, name);
605
+ const msg = `Successfully updated ${type} ${name}.`;
606
+ return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
428
607
  }
429
608
  case 'create': {
430
609
  const pkg = String(args.package ?? '$TMP');
@@ -436,6 +615,37 @@ async function handleSAPWrite(client, args) {
436
615
  const result = await createObject(client.http, client.safety, objectUrl, body, 'application/xml', transport);
437
616
  return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
438
617
  }
618
+ case 'edit_method': {
619
+ const method = String(args.method ?? '');
620
+ if (!method)
621
+ return errorResult('"method" is required for edit_method action.');
622
+ if (!source)
623
+ return errorResult('"source" (new method body) is required for edit_method action.');
624
+ if (type !== 'CLAS')
625
+ return errorResult('edit_method is only supported for type=CLAS.');
626
+ // Fetch current full source (use cache if available)
627
+ const currentSource = cachingLayer
628
+ ? (await cachingLayer.getSource('CLAS', name, () => client.getClass(name))).source
629
+ : await client.getClass(name);
630
+ // Use detected ABAP version from probe if available
631
+ const abaplintVer = cachedFeatures?.abapRelease
632
+ ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
633
+ : undefined;
634
+ // Splice in the new method body
635
+ const spliced = spliceMethod(currentSource, name, method, source, abaplintVer);
636
+ if (!spliced.success) {
637
+ return errorResult(spliced.error ?? `Failed to splice method "${method}" in ${name}.`);
638
+ }
639
+ // Pre-write lint validation on the full spliced source
640
+ const lintWarnings = runPreWriteLint(spliced.newSource, type, name, config);
641
+ if (lintWarnings.blocked)
642
+ return lintWarnings.result;
643
+ // Write the full source back (existing lock/modify/unlock flow)
644
+ await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced.newSource, transport);
645
+ cachingLayer?.invalidate(type, name);
646
+ const msg = `Successfully updated method "${method}" in ${type} ${name}.`;
647
+ return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
648
+ }
439
649
  case 'delete': {
440
650
  // Lock, delete, unlock pattern
441
651
  await client.http.withStatefulSession(async (session) => {
@@ -452,16 +662,81 @@ async function handleSAPWrite(client, args) {
452
662
  }
453
663
  }
454
664
  });
665
+ cachingLayer?.invalidate(type, name);
455
666
  return textResult(`Deleted ${type} ${name}.`);
456
667
  }
457
668
  default:
458
- return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete`);
669
+ return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method`);
670
+ }
671
+ }
672
+ /**
673
+ * Run pre-write lint validation on source code.
674
+ *
675
+ * This is a "lint-before-lock" optimization (pattern from vibing-steampunk):
676
+ * by validating locally before acquiring the SAP object lock, we avoid
677
+ * holding locks on objects that would fail validation anyway.
678
+ *
679
+ * Only runs a strict subset of correctness rules (parser_error, cloud_types, etc.)
680
+ * — not style/formatting rules. This prevents false rejections from opinionated
681
+ * style checks while catching genuine errors that would fail server-side anyway.
682
+ *
683
+ * If lint itself throws (e.g., abaplint bug on unusual syntax), we don't block
684
+ * the write — we let the SAP server-side syntax check handle it instead.
685
+ */
686
+ function runPreWriteLint(source, type, name, config) {
687
+ if (!config.lintBeforeWrite || !source) {
688
+ return { blocked: false };
689
+ }
690
+ try {
691
+ const filename = detectFilename(source, name);
692
+ const systemType = cachedFeatures?.systemType ?? (config.systemType !== 'auto' ? config.systemType : undefined);
693
+ const configOptions = {
694
+ systemType,
695
+ abapRelease: cachedFeatures?.abapRelease,
696
+ configFile: config.abaplintConfig,
697
+ };
698
+ const result = validateBeforeWrite(source, filename, configOptions);
699
+ if (!result.pass) {
700
+ const errorLines = result.errors.map((e) => ` Line ${e.line}: [${e.rule}] ${e.message}`).join('\n');
701
+ return {
702
+ blocked: true,
703
+ result: errorResult(`Pre-write lint check failed for ${type} ${name}. Fix these errors before writing:\n${errorLines}\n\n` +
704
+ 'Use SAPLint action="lint_and_fix" to auto-fix, or disable with --lint-before-write=false.'),
705
+ };
706
+ }
707
+ if (result.warnings.length > 0) {
708
+ const warningLines = result.warnings.map((w) => ` Line ${w.line}: [${w.rule}] ${w.message}`).join('\n');
709
+ return {
710
+ blocked: false,
711
+ warnings: `Lint warnings:\n${warningLines}`,
712
+ };
713
+ }
714
+ return { blocked: false };
715
+ }
716
+ catch {
717
+ // If lint itself fails, don't block the write
718
+ return { blocked: false };
459
719
  }
460
720
  }
461
721
  // ─── SAPActivate Handler ─────────────────────────────────────────────
462
722
  async function handleSAPActivate(client, args) {
463
- const name = String(args.name ?? '');
464
723
  const type = String(args.type ?? '');
724
+ // Batch activation: multiple objects at once (for RAP stacks etc.)
725
+ if (args.objects && Array.isArray(args.objects)) {
726
+ const objects = args.objects.map((o) => {
727
+ const objType = String(o.type ?? type);
728
+ const objName = String(o.name ?? '');
729
+ return { url: objectUrlForType(objType, objName), name: objName };
730
+ });
731
+ const result = await activateBatch(client.http, client.safety, objects);
732
+ const names = objects.map((o) => o.name).join(', ');
733
+ if (result.success) {
734
+ return textResult(`Successfully activated ${objects.length} objects: ${names}.${result.messages.length > 0 ? `\nMessages: ${result.messages.join('; ')}` : ''}`);
735
+ }
736
+ return errorResult(`Batch activation failed for: ${names}.\nErrors: ${result.messages.join('; ')}`);
737
+ }
738
+ // Single activation (existing behavior)
739
+ const name = String(args.name ?? '');
465
740
  const objectUrl = objectUrlForType(type, name);
466
741
  const result = await activate(client.http, client.safety, objectUrl);
467
742
  if (result.success) {
@@ -509,7 +784,31 @@ async function handleSAPNavigate(client, args) {
509
784
  if (!uri) {
510
785
  return errorResult('Provide uri or type+name to find references.');
511
786
  }
512
- const results = await findReferences(client.http, client.safety, uri);
787
+ const objectType = args.objectType ? String(args.objectType) : undefined;
788
+ let results;
789
+ try {
790
+ results = await findWhereUsed(client.http, client.safety, uri, objectType);
791
+ }
792
+ catch (err) {
793
+ // Only fall back for HTTP errors indicating the endpoint is not available (older SAP systems)
794
+ if (err instanceof AdtApiError && [404, 405, 415, 501].includes(err.statusCode)) {
795
+ results = await findReferences(client.http, client.safety, uri);
796
+ if (results.length === 0) {
797
+ return textResult('No references found.');
798
+ }
799
+ const json = JSON.stringify(results, null, 2);
800
+ if (objectType) {
801
+ return textResult(JSON.stringify({
802
+ note: `This SAP system does not support scope-based Where-Used. The objectType filter "${objectType}" was ignored — results below are unfiltered.`,
803
+ results,
804
+ }, null, 2));
805
+ }
806
+ return textResult(json);
807
+ }
808
+ else {
809
+ throw err;
810
+ }
811
+ }
513
812
  if (results.length === 0) {
514
813
  return textResult('No references found.');
515
814
  }
@@ -528,23 +827,64 @@ async function handleSAPDiagnose(client, args) {
528
827
  const action = String(args.action ?? '');
529
828
  const name = String(args.name ?? '');
530
829
  const type = String(args.type ?? '');
531
- const objectUrl = objectUrlForType(type, name);
532
830
  switch (action) {
533
831
  case 'syntax': {
832
+ const objectUrl = objectUrlForType(type, name);
534
833
  const result = await syntaxCheck(client.http, client.safety, objectUrl);
535
834
  return textResult(JSON.stringify(result, null, 2));
536
835
  }
537
836
  case 'unittest': {
837
+ const objectUrl = objectUrlForType(type, name);
538
838
  const results = await runUnitTests(client.http, client.safety, objectUrl);
539
839
  return textResult(JSON.stringify(results, null, 2));
540
840
  }
541
841
  case 'atc': {
842
+ const objectUrl = objectUrlForType(type, name);
542
843
  const variant = args.variant;
543
844
  const result = await runAtcCheck(client.http, client.safety, objectUrl, variant);
544
845
  return textResult(JSON.stringify(result, null, 2));
545
846
  }
847
+ case 'dumps': {
848
+ const id = args.id;
849
+ if (id) {
850
+ // Get single dump detail
851
+ const detail = await getDump(client.http, client.safety, id);
852
+ return textResult(JSON.stringify(detail, null, 2));
853
+ }
854
+ // List dumps
855
+ const user = args.user;
856
+ const maxResults = args.maxResults ? Number(args.maxResults) : undefined;
857
+ const dumps = await listDumps(client.http, client.safety, { user, maxResults });
858
+ return textResult(JSON.stringify(dumps, null, 2));
859
+ }
860
+ case 'traces': {
861
+ const id = args.id;
862
+ if (id) {
863
+ // Get trace analysis
864
+ const analysis = String(args.analysis ?? 'hitlist');
865
+ switch (analysis) {
866
+ case 'hitlist': {
867
+ const hitlist = await getTraceHitlist(client.http, client.safety, id);
868
+ return textResult(JSON.stringify(hitlist, null, 2));
869
+ }
870
+ case 'statements': {
871
+ const statements = await getTraceStatements(client.http, client.safety, id);
872
+ return textResult(JSON.stringify(statements, null, 2));
873
+ }
874
+ case 'dbAccesses': {
875
+ const dbAccesses = await getTraceDbAccesses(client.http, client.safety, id);
876
+ return textResult(JSON.stringify(dbAccesses, null, 2));
877
+ }
878
+ default:
879
+ return errorResult(`Unknown trace analysis type: ${analysis}. Supported: hitlist, statements, dbAccesses`);
880
+ }
881
+ }
882
+ // List traces
883
+ const traces = await listTraces(client.http, client.safety);
884
+ return textResult(JSON.stringify(traces, null, 2));
885
+ }
546
886
  default:
547
- return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc`);
887
+ return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc, dumps, traces`);
548
888
  }
549
889
  }
550
890
  // ─── SAPTransport Handler ────────────────────────────────────────────
@@ -584,14 +924,44 @@ async function handleSAPTransport(client, args) {
584
924
  }
585
925
  }
586
926
  // ─── SAPContext Handler ───────────────────────────────────────────────
587
- async function handleSAPContext(client, args) {
927
+ async function handleSAPContext(client, args, cachingLayer) {
928
+ const action = String(args.action ?? '');
588
929
  const type = String(args.type ?? '');
589
930
  const name = String(args.name ?? '');
590
931
  const maxDeps = Number(args.maxDeps ?? 20);
591
932
  const depth = Math.min(Math.max(Number(args.depth ?? 1), 1), 3);
933
+ // ─── Reverse dep lookup (pre-warmer only) ─────────────────────────
934
+ if (action === 'usages') {
935
+ if (!name)
936
+ return errorResult('"name" is required for usages action.');
937
+ if (!cachingLayer) {
938
+ return errorResult('Reverse dependency lookup requires object caching. Cache is disabled (ARC1_CACHE=none). ' +
939
+ 'Enable caching and run cache warmup to use this feature.');
940
+ }
941
+ const usages = cachingLayer.getUsages(name);
942
+ if (usages === null) {
943
+ return errorResult(`Reverse dependency lookup requires a pre-warmed cache. The cache warmup has not been run yet.\n\n` +
944
+ `To enable this feature:\n` +
945
+ `1. Start ARC-1 with --cache-warmup (or set ARC1_CACHE_WARMUP=true)\n` +
946
+ `2. Wait for the warmup to complete (indexes all custom objects)\n` +
947
+ `3. Then retry SAPContext(action="usages", name="${name}")\n\n` +
948
+ `Alternative: Use SAPNavigate(action="references", type="CLAS", name="${name}") for a live ADT lookup (slower, but works without warmup).`);
949
+ }
950
+ if (usages.length === 0) {
951
+ return textResult(`No objects found that depend on "${name}" in the cached index.`);
952
+ }
953
+ return textResult(JSON.stringify({ name, usageCount: usages.length, usages }, null, 2));
954
+ }
592
955
  if (!type || !name) {
593
956
  return errorResult('Both "type" and "name" are required for SAPContext.');
594
957
  }
958
+ // Helper: get source with cache support
959
+ const cachedGet = async (objType, objName, fetcher) => {
960
+ if (!cachingLayer)
961
+ return fetcher();
962
+ const { source } = await cachingLayer.getSource(objType, objName, fetcher);
963
+ return source;
964
+ };
595
965
  // Get source — either provided or fetched from SAP
596
966
  let source;
597
967
  if (args.source) {
@@ -600,37 +970,70 @@ async function handleSAPContext(client, args) {
600
970
  else {
601
971
  switch (type) {
602
972
  case 'CLAS':
603
- source = await client.getClass(name);
973
+ source = await cachedGet('CLAS', name, () => client.getClass(name));
604
974
  break;
605
975
  case 'INTF':
606
- source = await client.getInterface(name);
976
+ source = await cachedGet('INTF', name, () => client.getInterface(name));
607
977
  break;
608
978
  case 'PROG':
609
- source = await client.getProgram(name);
979
+ source = await cachedGet('PROG', name, () => client.getProgram(name));
610
980
  break;
611
981
  case 'FUNC': {
612
982
  const group = String(args.group ?? '');
613
983
  if (!group) {
614
984
  return errorResult('The "group" parameter is required for FUNC type. Use SAPSearch to find the function group.');
615
985
  }
616
- source = await client.getFunction(group, name);
986
+ source = await cachedGet('FUNC', name, () => client.getFunction(group, name));
617
987
  break;
618
988
  }
989
+ case 'DDLS': {
990
+ const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
991
+ const cdsResult = await compressCdsContext(client, ddlSource, name, maxDeps, depth, cachingLayer);
992
+ return textResult(cdsResult.output);
993
+ }
619
994
  default:
620
- return errorResult(`SAPContext supports types: CLAS, INTF, PROG, FUNC. Got: ${type}`);
995
+ return errorResult(`SAPContext supports types: CLAS, INTF, PROG, FUNC, DDLS. Got: ${type}`);
996
+ }
997
+ }
998
+ // Check dep graph cache — if source hash matches, return cached contracts
999
+ if (cachingLayer) {
1000
+ const cachedGraph = cachingLayer.getCachedDepGraph(source);
1001
+ if (cachedGraph) {
1002
+ const successful = cachedGraph.contracts.filter((c) => c.success);
1003
+ const failed = cachedGraph.contracts.filter((c) => !c.success);
1004
+ const lines = [];
1005
+ lines.push(`* === Dependency context for ${name} (${successful.length} deps resolved${failed.length > 0 ? `, ${failed.length} failed` : ''}) [cached] ===`);
1006
+ lines.push('');
1007
+ for (const contract of successful) {
1008
+ const typeLabel = contract.type.toLowerCase();
1009
+ const methodLabel = contract.methodCount > 0 ? `, ${contract.methodCount} methods` : '';
1010
+ lines.push(`* --- ${contract.name} (${typeLabel}${methodLabel}) ---`);
1011
+ lines.push(contract.source.trim());
1012
+ lines.push('');
1013
+ }
1014
+ if (failed.length > 0) {
1015
+ lines.push('* --- Failed dependencies ---');
1016
+ for (const f of failed) {
1017
+ lines.push(`* ${f.name}: ${f.error}`);
1018
+ }
1019
+ lines.push('');
1020
+ }
1021
+ const totalLines = lines.length;
1022
+ lines.push(`* Stats: ${successful.length + failed.length} deps found, ${successful.length} resolved, ${failed.length} failed, ${totalLines} lines [from cache]`);
1023
+ return textResult(lines.join('\n'));
621
1024
  }
622
1025
  }
623
1026
  // Use detected ABAP version from probe if available, otherwise Cloud (superset)
624
1027
  const abaplintVersion = cachedFeatures?.abapRelease
625
1028
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
626
1029
  : undefined;
627
- const result = await compressContext(client, source, name, type, maxDeps, depth, abaplintVersion);
1030
+ const result = await compressContext(client, source, name, type, maxDeps, depth, abaplintVersion, cachingLayer);
628
1031
  return textResult(result.output);
629
1032
  }
630
1033
  // ─── SAPManage Handler ────────────────────────────────────────────────
631
1034
  /** Cached feature status — populated on first probe */
632
1035
  let cachedFeatures;
633
- async function handleSAPManage(client, config, args) {
1036
+ async function handleSAPManage(client, config, args, cachingLayer) {
634
1037
  const action = String(args.action ?? '');
635
1038
  switch (action) {
636
1039
  case 'features': {
@@ -639,6 +1042,17 @@ async function handleSAPManage(client, config, args) {
639
1042
  }
640
1043
  return textResult(JSON.stringify(cachedFeatures, null, 2));
641
1044
  }
1045
+ case 'cache_stats': {
1046
+ if (!cachingLayer) {
1047
+ return textResult(JSON.stringify({ enabled: false, message: 'Object cache is disabled (ARC1_CACHE=none).' }));
1048
+ }
1049
+ const stats = cachingLayer.stats();
1050
+ return textResult(JSON.stringify({
1051
+ enabled: true,
1052
+ warmupAvailable: cachingLayer.isWarmupAvailable,
1053
+ ...stats,
1054
+ }, null, 2));
1055
+ }
642
1056
  case 'probe': {
643
1057
  const { defaultFeatureConfig } = await import('../adt/config.js');
644
1058
  const featureConfig = defaultFeatureConfig();
@@ -649,7 +1063,7 @@ async function handleSAPManage(client, config, args) {
649
1063
  featureConfig.amdp = config.featureAmdp;
650
1064
  featureConfig.ui5 = config.featureUi5;
651
1065
  featureConfig.transport = config.featureTransport;
652
- cachedFeatures = await probeFeatures(client.http, featureConfig);
1066
+ cachedFeatures = await probeFeatures(client.http, featureConfig, config.systemType);
653
1067
  return textResult(JSON.stringify(cachedFeatures, null, 2));
654
1068
  }
655
1069
  default:
@@ -660,4 +1074,8 @@ async function handleSAPManage(client, config, args) {
660
1074
  export function resetCachedFeatures() {
661
1075
  cachedFeatures = undefined;
662
1076
  }
1077
+ /** Set cached features directly (for testing BTP mode, etc.) */
1078
+ export function setCachedFeatures(features) {
1079
+ cachedFeatures = features;
1080
+ }
663
1081
  //# sourceMappingURL=intent.js.map