@linkup-ai/abap-ai 2.1.0 → 2.2.0
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/dist/adt-client.js +28 -9
- package/dist/cli/init.js +2 -2
- package/dist/cli/status.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.js +21 -3
- package/dist/license-guard.js +81 -0
- package/dist/security-policy.js +4 -4
- package/dist/tools/activate.js +3 -0
- package/dist/tools/read.js +6 -1
- package/dist/tools/system-info.js +11 -10
- package/dist/tools/transports.js +2 -1
- package/dist/tools/write.js +19 -0
- package/package.json +2 -2
package/dist/adt-client.js
CHANGED
|
@@ -49,19 +49,38 @@ function createHttpClient() {
|
|
|
49
49
|
const http = createHttpClient();
|
|
50
50
|
exports.http = http;
|
|
51
51
|
// ─── Sessão CSRF ──────────────────────────────────────────────────
|
|
52
|
+
/** Máximo de tentativas para obter CSRF token */
|
|
53
|
+
const CSRF_MAX_RETRIES = 3;
|
|
54
|
+
/** Intervalo entre tentativas de CSRF (ms) */
|
|
55
|
+
const CSRF_RETRY_DELAY = 1000;
|
|
52
56
|
async function ensureSession() {
|
|
53
57
|
if (session)
|
|
54
58
|
return session.csrfToken;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
let lastError;
|
|
60
|
+
for (let attempt = 1; attempt <= CSRF_MAX_RETRIES; attempt++) {
|
|
61
|
+
try {
|
|
62
|
+
const response = await http.get("/discovery", {
|
|
63
|
+
headers: { "X-CSRF-Token": "Fetch", Accept: "application/xml" },
|
|
64
|
+
validateStatus: (status) => status < 500, // 406 é esperado mas traz o CSRF token
|
|
65
|
+
});
|
|
66
|
+
const token = response.headers["x-csrf-token"];
|
|
67
|
+
if (token) {
|
|
68
|
+
session = { csrfToken: token };
|
|
69
|
+
return token;
|
|
70
|
+
}
|
|
71
|
+
lastError = new AdtError("CSRF token não retornado pelo SAP ADT.", 0, "/discovery");
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
lastError = error;
|
|
75
|
+
}
|
|
76
|
+
// Retry com backoff linear (1s, 2s, 3s)
|
|
77
|
+
if (attempt < CSRF_MAX_RETRIES) {
|
|
78
|
+
await new Promise((r) => setTimeout(r, CSRF_RETRY_DELAY * attempt));
|
|
79
|
+
}
|
|
62
80
|
}
|
|
63
|
-
|
|
64
|
-
|
|
81
|
+
throw lastError instanceof AdtError
|
|
82
|
+
? lastError
|
|
83
|
+
: new AdtError("Falha ao obter CSRF token do SAP ADT após 3 tentativas. Verifique se o serviço /sap/bc/adt/discovery está ativo.", 0, "/discovery");
|
|
65
84
|
}
|
|
66
85
|
function resetSession() {
|
|
67
86
|
session = null;
|
package/dist/cli/init.js
CHANGED
|
@@ -218,7 +218,7 @@ async function promptSystem() {
|
|
|
218
218
|
{
|
|
219
219
|
type: "select",
|
|
220
220
|
name: "environmentRole",
|
|
221
|
-
message: "Papel do ambiente (controla quais operações o LKPABAP.
|
|
221
|
+
message: "Papel do ambiente (controla quais operações o LKPABAP.ai pode executar)",
|
|
222
222
|
choices: [
|
|
223
223
|
{ title: "DEVELOPMENT — leitura + escrita + criação (padrão para DEV)", value: "DEVELOPMENT" },
|
|
224
224
|
{ title: "QUALITY — somente leitura (padrão para QAS/QA)", value: "QUALITY" },
|
|
@@ -252,7 +252,7 @@ async function promptSystem() {
|
|
|
252
252
|
async function init() {
|
|
253
253
|
console.log(`
|
|
254
254
|
╭─────────────────────────────────────╮
|
|
255
|
-
│ LKPABAP.
|
|
255
|
+
│ LKPABAP.ai — Setup │
|
|
256
256
|
│ Conecte o Claude ao seu SAP │
|
|
257
257
|
╰─────────────────────────────────────╯
|
|
258
258
|
`);
|
package/dist/cli/status.js
CHANGED
|
@@ -146,7 +146,7 @@ function countKnowledge() {
|
|
|
146
146
|
async function status() {
|
|
147
147
|
console.log(`
|
|
148
148
|
╭──────────────────────────────────────────────╮
|
|
149
|
-
│ LKPABAP.
|
|
149
|
+
│ LKPABAP.ai v${VERSION}${" ".repeat(30 - VERSION.length)}│
|
|
150
150
|
╰──────────────────────────────────────────────╯`);
|
|
151
151
|
// Licença
|
|
152
152
|
console.log(`
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ const remove_js_1 = require("./cli/remove.js");
|
|
|
8
8
|
const systems_js_1 = require("./cli/systems.js");
|
|
9
9
|
const VERSION = "2.0.0";
|
|
10
10
|
const HELP = `
|
|
11
|
-
LKPABAP.
|
|
11
|
+
LKPABAP.ai — AI-powered ABAP development for SAP
|
|
12
12
|
|
|
13
13
|
Uso:
|
|
14
14
|
abap-ai <comando> [opções]
|
|
@@ -38,7 +38,7 @@ async function main() {
|
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
if (command === "--version" || command === "-v") {
|
|
41
|
-
console.log(`LKPABAP.
|
|
41
|
+
console.log(`LKPABAP.ai v${VERSION}`);
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
switch (command) {
|
package/dist/index.js
CHANGED
|
@@ -57,10 +57,27 @@ const create_amdp_js_1 = require("./tools/create-amdp.js");
|
|
|
57
57
|
const system_profile_js_1 = require("./system-profile.js");
|
|
58
58
|
const security_policy_js_1 = require("./security-policy.js");
|
|
59
59
|
const security_audit_js_1 = require("./security-audit.js");
|
|
60
|
+
const license_guard_js_1 = require("./license-guard.js");
|
|
60
61
|
const server = new mcp_js_1.McpServer({
|
|
61
62
|
name: "abap-adt",
|
|
62
63
|
version: "2.0.0",
|
|
63
64
|
});
|
|
65
|
+
// ─── License Guard (wraps all tool handlers) ─────────────────────
|
|
66
|
+
// Intercepta server.tool para injetar licenseGuard() automaticamente.
|
|
67
|
+
// Toda tool retorna erro amigável se a licença é ausente ou expirada.
|
|
68
|
+
{
|
|
69
|
+
const _origTool = server.tool.bind(server);
|
|
70
|
+
server.tool = (...args) => {
|
|
71
|
+
const handler = args[args.length - 1];
|
|
72
|
+
args[args.length - 1] = async (...handlerArgs) => {
|
|
73
|
+
const licBlock = (0, license_guard_js_1.licenseGuard)();
|
|
74
|
+
if (licBlock)
|
|
75
|
+
return licBlock;
|
|
76
|
+
return await handler(...handlerArgs);
|
|
77
|
+
};
|
|
78
|
+
return _origTool.apply(server, args);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
64
81
|
// ─── Security Guard ───────────────────────────────────────────────
|
|
65
82
|
/**
|
|
66
83
|
* Verifica se uma tool pode ser executada sob a security policy atual.
|
|
@@ -94,8 +111,8 @@ function securityGuard(toolName, transportRequest) {
|
|
|
94
111
|
// Tool: abap_read
|
|
95
112
|
server.tool("abap_read", "Lê o código-fonte de um objeto ABAP no sistema SAP via ADT API. Para tabelas (TABL/DT), estruturas (TABL/DS) e table types (TTYP/TT), retorna a lista de campos com tipo e descrição.", {
|
|
96
113
|
object_type: zod_1.z
|
|
97
|
-
.enum(["PROG/P", "CLAS/OC", "FUGR/FF", "DOMA/D", "DTEL/D", "TABL/DT", "TABL/DS", "TTYP/TT", "INTF/OI", "DDLS/DF", "DDLX/MX", "SRVD/SRV", "BDEF/BDO", "SRVB/SVB"])
|
|
98
|
-
.describe("Tipo do objeto SAP. Ex: PROG/P (report), CLAS/OC (classe), DDLS/DF (CDS view), TABL/DT (tabela),
|
|
114
|
+
.enum(["PROG/P", "CLAS/OC", "FUGR/FF", "DOMA/D", "DTEL/D", "TABL/DT", "TABL/DS", "TTYP/TT", "INTF/OI", "DDLS/DF", "DDLX/MX", "SRVD/SRV", "BDEF/BDO", "SRVB/SVB", "MSAG/N", "ENHO/EO"])
|
|
115
|
+
.describe("Tipo do objeto SAP. Ex: PROG/P (report), CLAS/OC (classe), DDLS/DF (CDS view), TABL/DT (tabela), MSAG/N (classe de mensagens), ENHO/EO (enhancement implementation)."),
|
|
99
116
|
object_name: zod_1.z
|
|
100
117
|
.string()
|
|
101
118
|
.describe("Nome do objeto ABAP (ex: ZR_SD_PEDIDOS_ABERTOS)."),
|
|
@@ -1414,7 +1431,8 @@ async function main() {
|
|
|
1414
1431
|
await server.connect(transport);
|
|
1415
1432
|
const profile = (0, system_profile_js_1.getProfile)();
|
|
1416
1433
|
const policy = (0, security_policy_js_1.getPolicy)();
|
|
1417
|
-
|
|
1434
|
+
const licLabel = (0, license_guard_js_1.licenseStatusLabel)();
|
|
1435
|
+
process.stderr.write(`abap-mcp-server v2.2.0 iniciado | License: ${licLabel} | System: ${profile.system.type} | ABAP: ${profile.system.abapPlatform} | Security: ${policy.environment_role} (${policy.allowed_levels.join("+")})\n`);
|
|
1418
1436
|
}
|
|
1419
1437
|
main().catch((err) => {
|
|
1420
1438
|
process.stderr.write(`Falha ao iniciar servidor: ${err}\n`);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* License Guard — valida licença antes de executar qualquer tool.
|
|
4
|
+
*
|
|
5
|
+
* Singleton: lê ~/.abap-ai/license.json uma vez e cacheia.
|
|
6
|
+
* licenseGuard() retorna null se OK, ou MCP error response se bloqueado.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getLicense = getLicense;
|
|
10
|
+
exports.reloadLicense = reloadLicense;
|
|
11
|
+
exports.licenseGuard = licenseGuard;
|
|
12
|
+
exports.licenseStatusLabel = licenseStatusLabel;
|
|
13
|
+
const activate_js_1 = require("./cli/activate.js");
|
|
14
|
+
// ─── Singleton ───────────────────────────────────────────────────
|
|
15
|
+
let cachedLicense = undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Retorna a licença cacheada. null = arquivo não existe ou inválido.
|
|
18
|
+
*/
|
|
19
|
+
function getLicense() {
|
|
20
|
+
if (cachedLicense === undefined) {
|
|
21
|
+
cachedLicense = (0, activate_js_1.readLicense)();
|
|
22
|
+
}
|
|
23
|
+
return cachedLicense;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Força releitura do license.json (útil após activate).
|
|
27
|
+
*/
|
|
28
|
+
function reloadLicense() {
|
|
29
|
+
cachedLicense = undefined;
|
|
30
|
+
return getLicense();
|
|
31
|
+
}
|
|
32
|
+
function daysUntilExpiry(expiresStr) {
|
|
33
|
+
const expires = new Date(expiresStr + "T23:59:59");
|
|
34
|
+
const now = new Date();
|
|
35
|
+
return Math.ceil((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Verifica se a licença é válida.
|
|
39
|
+
* Retorna null se OK, ou MCP error response se bloqueado.
|
|
40
|
+
*/
|
|
41
|
+
function licenseGuard() {
|
|
42
|
+
const license = getLicense();
|
|
43
|
+
if (!license) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: "LKPABAP.ai — Licença não encontrada.\n\n" +
|
|
48
|
+
"Para usar as ferramentas, ative sua licença:\n\n" +
|
|
49
|
+
" abap-ai activate LK-XXXX-XXXX-XXXX\n\n" +
|
|
50
|
+
"Adquira em: https://lkpabap.ai",
|
|
51
|
+
}],
|
|
52
|
+
isError: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const daysLeft = daysUntilExpiry(license.expires);
|
|
56
|
+
if (daysLeft <= 0) {
|
|
57
|
+
return {
|
|
58
|
+
content: [{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: `LKPABAP.ai — Licença expirada em ${license.expires}.\n\n` +
|
|
61
|
+
"Renove sua licença:\n\n" +
|
|
62
|
+
" abap-ai activate <NOVA-CHAVE>\n\n" +
|
|
63
|
+
"Renove em: https://lkpabap.ai",
|
|
64
|
+
}],
|
|
65
|
+
isError: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Retorna string de status para log no startup.
|
|
72
|
+
*/
|
|
73
|
+
function licenseStatusLabel() {
|
|
74
|
+
const license = getLicense();
|
|
75
|
+
if (!license)
|
|
76
|
+
return "SEM LICENÇA";
|
|
77
|
+
const daysLeft = daysUntilExpiry(license.expires);
|
|
78
|
+
if (daysLeft <= 0)
|
|
79
|
+
return "EXPIRADA";
|
|
80
|
+
return `${license.plan} até ${license.expires}`;
|
|
81
|
+
}
|
package/dist/security-policy.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Security Policy — controle de acesso por camada de ambiente SAP.
|
|
4
4
|
*
|
|
5
|
-
* Implementa o princípio: "O LKPABAP.
|
|
5
|
+
* Implementa o princípio: "O LKPABAP.ai NUNCA sobrepõe as restrições de
|
|
6
6
|
* segurança do SAP. Em PRD, só leitura. Em QAS, leitura + debug.
|
|
7
7
|
* Em DEV, tudo liberado. abap_release_transport sempre bloqueado."
|
|
8
8
|
*
|
|
@@ -205,7 +205,7 @@ function checkToolAccess(toolName, policy) {
|
|
|
205
205
|
if (toolName === "abap_release_transport") {
|
|
206
206
|
return {
|
|
207
207
|
allowed: false,
|
|
208
|
-
reason: `BLOQUEADO: abap_release_transport é sempre bloqueado pelo LKPABAP.
|
|
208
|
+
reason: `BLOQUEADO: abap_release_transport é sempre bloqueado pelo LKPABAP.ai. `
|
|
209
209
|
+ `Liberar transportes é uma operação irreversível que pode propagar mudanças para ambientes produtivos. `
|
|
210
210
|
+ `Execute esta operação manualmente na transação SE09/SE10 do SAP GUI.`,
|
|
211
211
|
};
|
|
@@ -290,10 +290,10 @@ function listEnvironmentRoles() {
|
|
|
290
290
|
function describeEnvironmentRestriction(role) {
|
|
291
291
|
switch (role) {
|
|
292
292
|
case "PRODUCTION":
|
|
293
|
-
return "Em ambientes PRODUTIVOS, o LKPABAP.
|
|
293
|
+
return "Em ambientes PRODUTIVOS, o LKPABAP.ai opera somente em modo leitura. "
|
|
294
294
|
+ "Qualquer modificação deve ser feita via transporte a partir do ambiente de desenvolvimento.";
|
|
295
295
|
case "QUALITY":
|
|
296
|
-
return "Em ambientes de QUALIDADE (QAS), o LKPABAP.
|
|
296
|
+
return "Em ambientes de QUALIDADE (QAS), o LKPABAP.ai opera somente em modo leitura. "
|
|
297
297
|
+ "Modificações devem ser transportadas a partir do ambiente de desenvolvimento.";
|
|
298
298
|
case "DEVELOPMENT":
|
|
299
299
|
return ""; // DEV não tem restrição descritiva
|
package/dist/tools/activate.js
CHANGED
|
@@ -32,6 +32,9 @@ async function abapActivate(input) {
|
|
|
32
32
|
},
|
|
33
33
|
params: { method: "activate", preauditRequested: "true" },
|
|
34
34
|
responseType: "text",
|
|
35
|
+
// Aceita 200 (ativação OK) e 400 (erros de sintaxe) — sem isso, axios rejeita non-2xx
|
|
36
|
+
// e o erro pode não ser propagado corretamente pelo MCP SDK
|
|
37
|
+
validateStatus: (status) => status === 200 || status === 400,
|
|
35
38
|
});
|
|
36
39
|
const { success, alreadyActive, messages } = parseActivationResponse(response.data);
|
|
37
40
|
if (!success) {
|
package/dist/tools/read.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.abapReadTool = void 0;
|
|
|
4
4
|
exports.abapRead = abapRead;
|
|
5
5
|
const adt_client_js_1 = require("../adt-client.js");
|
|
6
6
|
const object_versions_js_1 = require("./object-versions.js");
|
|
7
|
+
const message_class_js_1 = require("./message-class.js");
|
|
7
8
|
function resolveSourcePath(adtPath, name, objectType, classInclude) {
|
|
8
9
|
if (objectType === "CLAS/OC" && classInclude && classInclude !== "main") {
|
|
9
10
|
return `/${adtPath}/${name}/includes/${classInclude}/source/main`;
|
|
@@ -106,6 +107,10 @@ async function abapRead(input) {
|
|
|
106
107
|
const { object_type, object_name, class_include } = input;
|
|
107
108
|
const name = object_name.toUpperCase();
|
|
108
109
|
const adtPath = (0, adt_client_js_1.resolveAdtPath)(object_type);
|
|
110
|
+
// MSAG/N: delegar para abapMessageClass que já faz parsing do XML
|
|
111
|
+
if (object_type === "MSAG/N") {
|
|
112
|
+
return (0, message_class_js_1.abapMessageClass)({ message_class_name: name });
|
|
113
|
+
}
|
|
109
114
|
// Para TABL/DT, TABL/DS e TTYP/TT: tentar /source/main primeiro (CDS DDL), se 404 ler metadados XML
|
|
110
115
|
if (DDIC_METADATA_TYPES.includes(object_type)) {
|
|
111
116
|
const nameLower = name.toLowerCase();
|
|
@@ -155,7 +160,7 @@ exports.abapReadTool = {
|
|
|
155
160
|
object_type: {
|
|
156
161
|
type: "string",
|
|
157
162
|
description: "Tipo do objeto SAP. Exemplos: PROG/P (report), CLAS/OC (classe), FUGR/FF (function module), DDLS/DF (CDS view), TABL/DT (tabela transparente), TABL/DS (estrutura), INTF/OI (interface).",
|
|
158
|
-
enum: ["PROG/P", "CLAS/OC", "FUGR/FF", "DOMA/D", "DTEL/D", "TABL/DT", "TABL/DS", "INTF/OI", "DDLS/DF", "DDLX/MX", "SRVD/SRV", "BDEF/BDO", "SRVB/SVB"],
|
|
163
|
+
enum: ["PROG/P", "CLAS/OC", "FUGR/FF", "DOMA/D", "DTEL/D", "TABL/DT", "TABL/DS", "TTYP/TT", "INTF/OI", "DDLS/DF", "DDLX/MX", "SRVD/SRV", "BDEF/BDO", "SRVB/SVB", "MSAG/N", "ENHO/EO"],
|
|
159
164
|
},
|
|
160
165
|
object_name: {
|
|
161
166
|
type: "string",
|
|
@@ -233,18 +233,19 @@ function detectSystemType(discoveryXml, basisVersion, platform) {
|
|
|
233
233
|
if (discoveryXml.includes("bw/modelingtools") || discoveryXml.includes("bw/querydesigner")) {
|
|
234
234
|
return "SAP_BW";
|
|
235
235
|
}
|
|
236
|
-
//
|
|
237
|
-
if (bv >= 750) {
|
|
238
|
-
// Verifica se tem RAP-related collections (mais provável S/4)
|
|
239
|
-
if (discoveryXml.includes("behaviordefinitions") || discoveryXml.includes("ServiceBindings")) {
|
|
240
|
-
return "ON_PREMISE_S4";
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
// ECC ou sistema antigo
|
|
236
|
+
// ECC ou sistema antigo (BASIS < 750 é sempre ECC)
|
|
244
237
|
if (bv < 750)
|
|
245
238
|
return "ON_PREMISE_ECC";
|
|
246
|
-
//
|
|
247
|
-
|
|
239
|
+
// BASIS 750+: distinguir S/4HANA de ECC EHP8 (ambos têm BASIS 750)
|
|
240
|
+
// S/4HANA sempre inclui collections RAP/Service no discovery XML.
|
|
241
|
+
// ECC 750 NÃO tem esses endpoints — essa é a diferença chave.
|
|
242
|
+
const hasS4Collections = discoveryXml.includes("behaviordefinitions") ||
|
|
243
|
+
discoveryXml.includes("ServiceBindings") ||
|
|
244
|
+
discoveryXml.includes("ddic/srvd/sources");
|
|
245
|
+
if (hasS4Collections)
|
|
246
|
+
return "ON_PREMISE_S4";
|
|
247
|
+
// BASIS 750 sem collections S/4 = ECC EHP8
|
|
248
|
+
return "ON_PREMISE_ECC";
|
|
248
249
|
}
|
|
249
250
|
function detectRelease(basisVersion) {
|
|
250
251
|
const bv = parseInt(basisVersion, 10) || 0;
|
package/dist/tools/transports.js
CHANGED
|
@@ -29,7 +29,8 @@ async function abapListTransports(input) {
|
|
|
29
29
|
});
|
|
30
30
|
const requests = parseTransportResponse(response.data);
|
|
31
31
|
if (requests.length === 0) {
|
|
32
|
-
return `Nenhuma ordem de transporte aberta encontrada para ${user.toUpperCase()}
|
|
32
|
+
return `Nenhuma ordem de transporte aberta encontrada para ${user.toUpperCase()}.\n` +
|
|
33
|
+
`Para criar uma nova request, use abap_create_transport ou crie manualmente via SE09/SE10 no SAP GUI.`;
|
|
33
34
|
}
|
|
34
35
|
const header = `Ordens de transporte abertas — ${user.toUpperCase()} (${requests.length}):\n`;
|
|
35
36
|
const rows = requests.map((r) => {
|
package/dist/tools/write.js
CHANGED
|
@@ -31,6 +31,12 @@ async function abapWrite(input) {
|
|
|
31
31
|
await (0, adt_client_js_1.adtPut)(sourcePath, source, etag, transport_request);
|
|
32
32
|
}
|
|
33
33
|
catch (putError) {
|
|
34
|
+
// Detectar erro de transport request obrigatória
|
|
35
|
+
if (isTransportRequiredError(putError) && !transport_request) {
|
|
36
|
+
throw new Error(`Objeto ${name} requer transport request para gravação (pacote não-local).\n` +
|
|
37
|
+
`Use abap_list_transports para verificar requests abertas, ou abap_create_transport para criar uma nova.\n` +
|
|
38
|
+
`Depois, informe o parâmetro transport_request (ex: "DEVK900123").`);
|
|
39
|
+
}
|
|
34
40
|
// Workaround: em on-premise S/4, o GET retorna ETag com sufixo diferente do esperado pelo PUT.
|
|
35
41
|
// Se o PUT falha com 412, extrai o ETag correto da mensagem de erro e tenta novamente.
|
|
36
42
|
if ((0, system_profile_js_1.getProfile)().quirks.etagRequiresManualFix && is412Error(putError)) {
|
|
@@ -54,6 +60,19 @@ async function abapWrite(input) {
|
|
|
54
60
|
}
|
|
55
61
|
return `Objeto ${object_name} gravado com sucesso.`;
|
|
56
62
|
}
|
|
63
|
+
/** Detecta erros do SAP indicando que transport request é obrigatória */
|
|
64
|
+
function isTransportRequiredError(error) {
|
|
65
|
+
if (error instanceof adt_client_js_1.AdtError) {
|
|
66
|
+
const msg = (error.sapMessage ?? error.message).toLowerCase();
|
|
67
|
+
return msg.includes("correction") || msg.includes("transport") || msg.includes("request");
|
|
68
|
+
}
|
|
69
|
+
const data = error.response?.data;
|
|
70
|
+
if (typeof data === "string") {
|
|
71
|
+
const lower = data.toLowerCase();
|
|
72
|
+
return lower.includes("correction") || lower.includes("transport request") || lower.includes("corrNr");
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
57
76
|
function is412Error(error) {
|
|
58
77
|
return typeof error === "object" && error !== null &&
|
|
59
78
|
"response" in error &&
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linkup-ai/abap-ai",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "LKPABAP.
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "LKPABAP.ai — AI-powered ABAP development tools for SAP S/4HANA via ADT REST API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"abap-ai": "dist/cli.js"
|