arc-1 0.3.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.
- package/README.md +77 -199
- package/dist/adt/btp.d.ts +2 -2
- package/dist/adt/btp.d.ts.map +1 -1
- package/dist/adt/btp.js +1 -1
- package/dist/adt/btp.js.map +1 -1
- package/dist/adt/client.d.ts +13 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +39 -1
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/codeintel.d.ts +41 -0
- package/dist/adt/codeintel.d.ts.map +1 -1
- package/dist/adt/codeintel.js +99 -27
- package/dist/adt/codeintel.js.map +1 -1
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/cookies.d.ts.map +1 -1
- package/dist/adt/cookies.js.map +1 -1
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/devtools.d.ts +14 -0
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +28 -3
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/diagnostics.d.ts +101 -0
- package/dist/adt/diagnostics.d.ts.map +1 -0
- package/dist/adt/diagnostics.js +349 -0
- package/dist/adt/diagnostics.js.map +1 -0
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/features.d.ts.map +1 -1
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/http.d.ts +26 -7
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +140 -86
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/oauth.d.ts.map +1 -1
- package/dist/adt/oauth.js.map +1 -1
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/safety.js.map +1 -1
- package/dist/adt/transport.d.ts.map +1 -1
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +139 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/types.js.map +1 -1
- package/dist/adt/xml-parser.d.ts +37 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +147 -0
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/cache/cache.d.ts +47 -4
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js +16 -5
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/caching-layer.d.ts +82 -0
- package/dist/cache/caching-layer.d.ts.map +1 -0
- package/dist/cache/caching-layer.js +134 -0
- package/dist/cache/caching-layer.js.map +1 -0
- package/dist/cache/memory.d.ts +14 -2
- package/dist/cache/memory.d.ts.map +1 -1
- package/dist/cache/memory.js +72 -5
- package/dist/cache/memory.js.map +1 -1
- package/dist/cache/sqlite.d.ts +10 -1
- package/dist/cache/sqlite.d.ts.map +1 -1
- package/dist/cache/sqlite.js +90 -2
- package/dist/cache/sqlite.js.map +1 -1
- package/dist/cache/warmup.d.ts +41 -0
- package/dist/cache/warmup.d.ts.map +1 -0
- package/dist/cache/warmup.js +286 -0
- package/dist/cache/warmup.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/context/cds-deps.d.ts +35 -0
- package/dist/context/cds-deps.d.ts.map +1 -0
- package/dist/context/cds-deps.js +201 -0
- package/dist/context/cds-deps.js.map +1 -0
- package/dist/context/compressor.d.ts +20 -1
- package/dist/context/compressor.d.ts.map +1 -1
- package/dist/context/compressor.js +178 -17
- package/dist/context/compressor.js.map +1 -1
- package/dist/context/contract.d.ts.map +1 -1
- package/dist/context/contract.js.map +1 -1
- package/dist/context/deps.d.ts.map +1 -1
- package/dist/context/deps.js.map +1 -1
- package/dist/context/method-surgery.d.ts +91 -0
- package/dist/context/method-surgery.d.ts.map +1 -0
- package/dist/context/method-surgery.js +441 -0
- package/dist/context/method-surgery.js.map +1 -0
- package/dist/context/types.d.ts +7 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/context/types.js.map +1 -1
- package/dist/handlers/hyperfocused.d.ts +35 -0
- package/dist/handlers/hyperfocused.d.ts.map +1 -0
- package/dist/handlers/hyperfocused.js +104 -0
- package/dist/handlers/hyperfocused.js.map +1 -0
- package/dist/handlers/intent.d.ts +2 -1
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +440 -43
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +130 -39
- package/dist/handlers/tools.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lint/config-builder.d.ts +60 -0
- package/dist/lint/config-builder.d.ts.map +1 -0
- package/dist/lint/config-builder.js +187 -0
- package/dist/lint/config-builder.js.map +1 -0
- package/dist/lint/lint.d.ts +43 -0
- package/dist/lint/lint.d.ts.map +1 -1
- package/dist/lint/lint.js +77 -2
- package/dist/lint/lint.js.map +1 -1
- package/dist/lint/presets/cloud.d.ts +16 -0
- package/dist/lint/presets/cloud.d.ts.map +1 -0
- package/dist/lint/presets/cloud.js +111 -0
- package/dist/lint/presets/cloud.js.map +1 -0
- package/dist/lint/presets/onprem.d.ts +17 -0
- package/dist/lint/presets/onprem.d.ts.map +1 -0
- package/dist/lint/presets/onprem.js +82 -0
- package/dist/lint/presets/onprem.js.map +1 -0
- package/dist/server/audit.d.ts +1 -0
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +12 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js.map +1 -1
- package/dist/server/elicit.d.ts.map +1 -1
- package/dist/server/elicit.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +4 -1
- package/dist/server/http.js.map +1 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js.map +1 -1
- package/dist/server/server.d.ts +3 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +90 -5
- package/dist/server/server.js.map +1 -1
- package/dist/server/sinks/btp-auditlog.d.ts.map +1 -1
- package/dist/server/sinks/btp-auditlog.js.map +1 -1
- package/dist/server/sinks/file.d.ts.map +1 -1
- package/dist/server/sinks/file.js.map +1 -1
- package/dist/server/sinks/stderr.d.ts.map +1 -1
- package/dist/server/sinks/stderr.js.map +1 -1
- package/dist/server/sinks/types.d.ts.map +1 -1
- package/dist/server/sinks/types.js.map +1 -1
- package/dist/server/types.d.ts +14 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +6 -0
- package/dist/server/types.js.map +1 -1
- package/dist/server/xsuaa.d.ts.map +1 -1
- package/dist/server/xsuaa.js.map +1 -1
- package/package.json +13 -6
package/dist/handlers/intent.js
CHANGED
|
@@ -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 {
|
|
19
|
-
import {
|
|
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,
|
|
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,
|
|
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
|
});
|
|
@@ -215,31 +241,64 @@ const BTP_HINTS = {
|
|
|
215
241
|
TEXT_ELEMENTS: 'Text elements are not available on BTP ABAP Environment (no classic programs). Use message classes or constant classes instead.',
|
|
216
242
|
VARIANTS: 'Variants are not available on BTP ABAP Environment (no classic programs).',
|
|
217
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.',
|
|
218
245
|
};
|
|
219
|
-
async function handleSAPRead(client, args) {
|
|
246
|
+
async function handleSAPRead(client, args, cachingLayer) {
|
|
220
247
|
const type = String(args.type ?? '');
|
|
221
248
|
const name = String(args.name ?? '');
|
|
222
249
|
// BTP: return helpful error for unavailable types
|
|
223
250
|
if (isBtpSystem() && BTP_HINTS[type]) {
|
|
224
251
|
return errorResult(BTP_HINTS[type]);
|
|
225
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
|
+
};
|
|
226
260
|
switch (type) {
|
|
227
261
|
case 'PROG':
|
|
228
|
-
return textResult(await client.getProgram(name));
|
|
229
|
-
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
|
+
}
|
|
230
285
|
return textResult(await client.getClass(name, args.include));
|
|
286
|
+
}
|
|
231
287
|
case 'INTF':
|
|
232
|
-
return textResult(await client.getInterface(name));
|
|
288
|
+
return textResult(await cachedGet('INTF', name, () => client.getInterface(name)));
|
|
233
289
|
case 'FUNC': {
|
|
234
290
|
let group = String(args.group ?? '');
|
|
235
291
|
if (!group) {
|
|
236
|
-
|
|
292
|
+
// Use cached func group resolution if available
|
|
293
|
+
const resolved = cachingLayer
|
|
294
|
+
? await cachingLayer.resolveFuncGroup(client, name)
|
|
295
|
+
: await client.resolveFunctionGroup(name);
|
|
237
296
|
if (!resolved) {
|
|
238
297
|
return errorResult(`Cannot resolve function group for "${name}". Provide the group parameter explicitly, or use SAPSearch("${name}") to find the function group.`);
|
|
239
298
|
}
|
|
240
299
|
group = resolved;
|
|
241
300
|
}
|
|
242
|
-
return textResult(await client.getFunction(group, name));
|
|
301
|
+
return textResult(await cachedGet('FUNC', name, () => client.getFunction(group, name)));
|
|
243
302
|
}
|
|
244
303
|
case 'FUGR': {
|
|
245
304
|
const expand = Boolean(args.expand_includes);
|
|
@@ -265,17 +324,53 @@ async function handleSAPRead(client, args) {
|
|
|
265
324
|
return textResult(JSON.stringify(fg, null, 2));
|
|
266
325
|
}
|
|
267
326
|
case 'INCL':
|
|
268
|
-
return textResult(await client.getInclude(name));
|
|
269
|
-
case 'DDLS':
|
|
270
|
-
|
|
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
|
+
}
|
|
271
335
|
case 'BDEF':
|
|
272
|
-
return textResult(await client.getBdef(name));
|
|
336
|
+
return textResult(await cachedGet('BDEF', name, () => client.getBdef(name)));
|
|
273
337
|
case 'SRVD':
|
|
274
|
-
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)));
|
|
275
343
|
case 'TABL':
|
|
276
|
-
return textResult(await client.getTable(name));
|
|
344
|
+
return textResult(await cachedGet('TABL', name, () => client.getTable(name)));
|
|
277
345
|
case 'VIEW':
|
|
278
|
-
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
|
+
}
|
|
279
374
|
case 'TABLE_CONTENTS': {
|
|
280
375
|
const maxRows = Number(args.maxRows ?? 100);
|
|
281
376
|
const data = await client.getTableContents(name, maxRows, args.sqlFilter);
|
|
@@ -326,7 +421,7 @@ async function handleSAPRead(client, args) {
|
|
|
326
421
|
case 'VARIANTS':
|
|
327
422
|
return textResult(await client.getVariants(name));
|
|
328
423
|
default:
|
|
329
|
-
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`);
|
|
330
425
|
}
|
|
331
426
|
}
|
|
332
427
|
async function handleSAPSearch(client, args) {
|
|
@@ -382,20 +477,69 @@ async function handleSAPQuery(client, args) {
|
|
|
382
477
|
throw err;
|
|
383
478
|
}
|
|
384
479
|
}
|
|
385
|
-
|
|
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) {
|
|
386
483
|
const action = String(args.action ?? '');
|
|
484
|
+
const ruleOverrides = args.rules;
|
|
485
|
+
const configOptions = buildLintConfigOptions(config, ruleOverrides);
|
|
387
486
|
switch (action) {
|
|
388
487
|
case 'lint': {
|
|
389
488
|
const source = String(args.source ?? '');
|
|
489
|
+
if (!source)
|
|
490
|
+
return errorResult('"source" is required for lint action.');
|
|
390
491
|
const name = String(args.name ?? 'UNKNOWN');
|
|
391
492
|
const filename = detectFilename(source, name);
|
|
392
|
-
const
|
|
493
|
+
const lintConfig = buildLintConfig(configOptions);
|
|
494
|
+
const issues = lintAbapSource(source, filename, lintConfig);
|
|
393
495
|
return textResult(JSON.stringify(issues, null, 2));
|
|
394
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
|
+
}
|
|
395
521
|
default:
|
|
396
|
-
return errorResult(`Unknown SAPLint action: ${action}. Supported: lint, atc,
|
|
522
|
+
return errorResult(`Unknown SAPLint action: "${action}". Supported: lint, lint_and_fix, list_rules. For atc/syntax/unittest, use SAPDiagnose instead.`);
|
|
397
523
|
}
|
|
398
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
|
+
}
|
|
399
543
|
// ─── Object URL Mapping ──────────────────────────────────────────────
|
|
400
544
|
/** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. */
|
|
401
545
|
function objectUrlForType(type, name) {
|
|
@@ -419,8 +563,20 @@ function objectUrlForType(type, name) {
|
|
|
419
563
|
return `/sap/bc/adt/bo/behaviordefinitions/${encoded}`;
|
|
420
564
|
case 'SRVD':
|
|
421
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}`;
|
|
422
570
|
case 'TABL':
|
|
423
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}`;
|
|
424
580
|
default:
|
|
425
581
|
return `/sap/bc/adt/programs/programs/${encoded}`;
|
|
426
582
|
}
|
|
@@ -430,7 +586,7 @@ function sourceUrlForType(type, name) {
|
|
|
430
586
|
return `${objectUrlForType(type, name)}/source/main`;
|
|
431
587
|
}
|
|
432
588
|
// ─── SAPWrite Handler ────────────────────────────────────────────────
|
|
433
|
-
async function handleSAPWrite(client, args) {
|
|
589
|
+
async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
434
590
|
const action = String(args.action ?? '');
|
|
435
591
|
const type = String(args.type ?? '');
|
|
436
592
|
const name = String(args.name ?? '');
|
|
@@ -440,8 +596,14 @@ async function handleSAPWrite(client, args) {
|
|
|
440
596
|
const srcUrl = sourceUrlForType(type, name);
|
|
441
597
|
switch (action) {
|
|
442
598
|
case 'update': {
|
|
599
|
+
// Pre-write lint validation
|
|
600
|
+
const lintWarnings = runPreWriteLint(source, type, name, config);
|
|
601
|
+
if (lintWarnings.blocked)
|
|
602
|
+
return lintWarnings.result;
|
|
443
603
|
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport);
|
|
444
|
-
|
|
604
|
+
cachingLayer?.invalidate(type, name);
|
|
605
|
+
const msg = `Successfully updated ${type} ${name}.`;
|
|
606
|
+
return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
|
|
445
607
|
}
|
|
446
608
|
case 'create': {
|
|
447
609
|
const pkg = String(args.package ?? '$TMP');
|
|
@@ -453,6 +615,37 @@ async function handleSAPWrite(client, args) {
|
|
|
453
615
|
const result = await createObject(client.http, client.safety, objectUrl, body, 'application/xml', transport);
|
|
454
616
|
return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
|
|
455
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
|
+
}
|
|
456
649
|
case 'delete': {
|
|
457
650
|
// Lock, delete, unlock pattern
|
|
458
651
|
await client.http.withStatefulSession(async (session) => {
|
|
@@ -469,16 +662,81 @@ async function handleSAPWrite(client, args) {
|
|
|
469
662
|
}
|
|
470
663
|
}
|
|
471
664
|
});
|
|
665
|
+
cachingLayer?.invalidate(type, name);
|
|
472
666
|
return textResult(`Deleted ${type} ${name}.`);
|
|
473
667
|
}
|
|
474
668
|
default:
|
|
475
|
-
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 };
|
|
476
719
|
}
|
|
477
720
|
}
|
|
478
721
|
// ─── SAPActivate Handler ─────────────────────────────────────────────
|
|
479
722
|
async function handleSAPActivate(client, args) {
|
|
480
|
-
const name = String(args.name ?? '');
|
|
481
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 ?? '');
|
|
482
740
|
const objectUrl = objectUrlForType(type, name);
|
|
483
741
|
const result = await activate(client.http, client.safety, objectUrl);
|
|
484
742
|
if (result.success) {
|
|
@@ -526,7 +784,31 @@ async function handleSAPNavigate(client, args) {
|
|
|
526
784
|
if (!uri) {
|
|
527
785
|
return errorResult('Provide uri or type+name to find references.');
|
|
528
786
|
}
|
|
529
|
-
const
|
|
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
|
+
}
|
|
530
812
|
if (results.length === 0) {
|
|
531
813
|
return textResult('No references found.');
|
|
532
814
|
}
|
|
@@ -545,23 +827,64 @@ async function handleSAPDiagnose(client, args) {
|
|
|
545
827
|
const action = String(args.action ?? '');
|
|
546
828
|
const name = String(args.name ?? '');
|
|
547
829
|
const type = String(args.type ?? '');
|
|
548
|
-
const objectUrl = objectUrlForType(type, name);
|
|
549
830
|
switch (action) {
|
|
550
831
|
case 'syntax': {
|
|
832
|
+
const objectUrl = objectUrlForType(type, name);
|
|
551
833
|
const result = await syntaxCheck(client.http, client.safety, objectUrl);
|
|
552
834
|
return textResult(JSON.stringify(result, null, 2));
|
|
553
835
|
}
|
|
554
836
|
case 'unittest': {
|
|
837
|
+
const objectUrl = objectUrlForType(type, name);
|
|
555
838
|
const results = await runUnitTests(client.http, client.safety, objectUrl);
|
|
556
839
|
return textResult(JSON.stringify(results, null, 2));
|
|
557
840
|
}
|
|
558
841
|
case 'atc': {
|
|
842
|
+
const objectUrl = objectUrlForType(type, name);
|
|
559
843
|
const variant = args.variant;
|
|
560
844
|
const result = await runAtcCheck(client.http, client.safety, objectUrl, variant);
|
|
561
845
|
return textResult(JSON.stringify(result, null, 2));
|
|
562
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
|
+
}
|
|
563
886
|
default:
|
|
564
|
-
return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc`);
|
|
887
|
+
return errorResult(`Unknown SAPDiagnose action: ${action}. Supported: syntax, unittest, atc, dumps, traces`);
|
|
565
888
|
}
|
|
566
889
|
}
|
|
567
890
|
// ─── SAPTransport Handler ────────────────────────────────────────────
|
|
@@ -601,14 +924,44 @@ async function handleSAPTransport(client, args) {
|
|
|
601
924
|
}
|
|
602
925
|
}
|
|
603
926
|
// ─── SAPContext Handler ───────────────────────────────────────────────
|
|
604
|
-
async function handleSAPContext(client, args) {
|
|
927
|
+
async function handleSAPContext(client, args, cachingLayer) {
|
|
928
|
+
const action = String(args.action ?? '');
|
|
605
929
|
const type = String(args.type ?? '');
|
|
606
930
|
const name = String(args.name ?? '');
|
|
607
931
|
const maxDeps = Number(args.maxDeps ?? 20);
|
|
608
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
|
+
}
|
|
609
955
|
if (!type || !name) {
|
|
610
956
|
return errorResult('Both "type" and "name" are required for SAPContext.');
|
|
611
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
|
+
};
|
|
612
965
|
// Get source — either provided or fetched from SAP
|
|
613
966
|
let source;
|
|
614
967
|
if (args.source) {
|
|
@@ -617,37 +970,70 @@ async function handleSAPContext(client, args) {
|
|
|
617
970
|
else {
|
|
618
971
|
switch (type) {
|
|
619
972
|
case 'CLAS':
|
|
620
|
-
source = await client.getClass(name);
|
|
973
|
+
source = await cachedGet('CLAS', name, () => client.getClass(name));
|
|
621
974
|
break;
|
|
622
975
|
case 'INTF':
|
|
623
|
-
source = await client.getInterface(name);
|
|
976
|
+
source = await cachedGet('INTF', name, () => client.getInterface(name));
|
|
624
977
|
break;
|
|
625
978
|
case 'PROG':
|
|
626
|
-
source = await client.getProgram(name);
|
|
979
|
+
source = await cachedGet('PROG', name, () => client.getProgram(name));
|
|
627
980
|
break;
|
|
628
981
|
case 'FUNC': {
|
|
629
982
|
const group = String(args.group ?? '');
|
|
630
983
|
if (!group) {
|
|
631
984
|
return errorResult('The "group" parameter is required for FUNC type. Use SAPSearch to find the function group.');
|
|
632
985
|
}
|
|
633
|
-
source = await client.getFunction(group, name);
|
|
986
|
+
source = await cachedGet('FUNC', name, () => client.getFunction(group, name));
|
|
634
987
|
break;
|
|
635
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
|
+
}
|
|
636
994
|
default:
|
|
637
|
-
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'));
|
|
638
1024
|
}
|
|
639
1025
|
}
|
|
640
1026
|
// Use detected ABAP version from probe if available, otherwise Cloud (superset)
|
|
641
1027
|
const abaplintVersion = cachedFeatures?.abapRelease
|
|
642
1028
|
? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
|
|
643
1029
|
: undefined;
|
|
644
|
-
const result = await compressContext(client, source, name, type, maxDeps, depth, abaplintVersion);
|
|
1030
|
+
const result = await compressContext(client, source, name, type, maxDeps, depth, abaplintVersion, cachingLayer);
|
|
645
1031
|
return textResult(result.output);
|
|
646
1032
|
}
|
|
647
1033
|
// ─── SAPManage Handler ────────────────────────────────────────────────
|
|
648
1034
|
/** Cached feature status — populated on first probe */
|
|
649
1035
|
let cachedFeatures;
|
|
650
|
-
async function handleSAPManage(client, config, args) {
|
|
1036
|
+
async function handleSAPManage(client, config, args, cachingLayer) {
|
|
651
1037
|
const action = String(args.action ?? '');
|
|
652
1038
|
switch (action) {
|
|
653
1039
|
case 'features': {
|
|
@@ -656,6 +1042,17 @@ async function handleSAPManage(client, config, args) {
|
|
|
656
1042
|
}
|
|
657
1043
|
return textResult(JSON.stringify(cachedFeatures, null, 2));
|
|
658
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
|
+
}
|
|
659
1056
|
case 'probe': {
|
|
660
1057
|
const { defaultFeatureConfig } = await import('../adt/config.js');
|
|
661
1058
|
const featureConfig = defaultFeatureConfig();
|