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.
- package/LICENSE +1 -0
- 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 +40 -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 +6 -0
- 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 +11 -2
- package/dist/adt/features.d.ts.map +1 -1
- package/dist/adt/features.js +37 -12
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/http.d.ts +33 -7
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +148 -84
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/oauth.d.ts +120 -0
- package/dist/adt/oauth.d.ts.map +1 -0
- package/dist/adt/oauth.js +321 -0
- package/dist/adt/oauth.js.map +1 -0
- 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 +143 -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 +5 -1
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +462 -44
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/tools.d.ts +5 -0
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +270 -87
- 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 +20 -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 +4 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +121 -8
- 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 +19 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +8 -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 +14 -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
|
});
|
|
@@ -203,26 +229,76 @@ export async function handleToolCall(client, _config, toolName, args, authInfo,
|
|
|
203
229
|
});
|
|
204
230
|
}
|
|
205
231
|
// ─── Individual Tool Handlers ────────────────────────────────────────
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|