@linkup-ai/abap-ai 2.1.0 → 2.2.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.
@@ -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
- const response = await http.get("/discovery", {
56
- headers: { "X-CSRF-Token": "Fetch", Accept: "application/xml" },
57
- validateStatus: (status) => status < 500, // 406 é esperado mas traz o CSRF token
58
- });
59
- const token = response.headers["x-csrf-token"];
60
- if (!token) {
61
- throw new AdtError("Falha ao obter CSRF token do SAP ADT. Verifique se o serviço /sap/bc/adt/discovery está ativo.", 0, "/discovery");
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
- session = { csrfToken: token };
64
- return token;
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
@@ -42,7 +42,11 @@ const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  const https = __importStar(require("https"));
44
44
  const http = __importStar(require("http"));
45
- const MCP_JSON_PATH = path.join(process.env.HOME || "~", ".claude", "mcp.json");
45
+ const GLOBAL_MCP_PATH = path.join(process.env.HOME || "~", ".claude", "mcp.json");
46
+ const LOCAL_MCP_PATH = path.join(process.cwd(), ".claude", "mcp.json");
47
+ function getMcpPath(local) {
48
+ return local ? LOCAL_MCP_PATH : GLOBAL_MCP_PATH;
49
+ }
46
50
  // ---------------------------------------------------------------------------
47
51
  // Teste de conexão SAP
48
52
  // ---------------------------------------------------------------------------
@@ -115,21 +119,21 @@ async function testConnection(system) {
115
119
  // ---------------------------------------------------------------------------
116
120
  // mcp.json management
117
121
  // ---------------------------------------------------------------------------
118
- function readMcpConfig() {
122
+ function readMcpConfig(mcpPath) {
119
123
  try {
120
- const raw = fs.readFileSync(MCP_JSON_PATH, "utf-8");
124
+ const raw = fs.readFileSync(mcpPath, "utf-8");
121
125
  return JSON.parse(raw);
122
126
  }
123
127
  catch {
124
128
  return { mcpServers: {} };
125
129
  }
126
130
  }
127
- function writeMcpConfig(config) {
128
- const dir = path.dirname(MCP_JSON_PATH);
131
+ function writeMcpConfig(config, mcpPath) {
132
+ const dir = path.dirname(mcpPath);
129
133
  if (!fs.existsSync(dir)) {
130
134
  fs.mkdirSync(dir, { recursive: true });
131
135
  }
132
- fs.writeFileSync(MCP_JSON_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
136
+ fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
133
137
  }
134
138
  function detectServerPath() {
135
139
  // Tentar encontrar o index.js relativo ao CLI
@@ -218,7 +222,7 @@ async function promptSystem() {
218
222
  {
219
223
  type: "select",
220
224
  name: "environmentRole",
221
- message: "Papel do ambiente (controla quais operações o LKPABAP.ia pode executar)",
225
+ message: "Papel do ambiente (controla quais operações o LKPABAP.ai pode executar)",
222
226
  choices: [
223
227
  { title: "DEVELOPMENT — leitura + escrita + criação (padrão para DEV)", value: "DEVELOPMENT" },
224
228
  { title: "QUALITY — somente leitura (padrão para QAS/QA)", value: "QUALITY" },
@@ -249,14 +253,19 @@ async function promptSystem() {
249
253
  // ---------------------------------------------------------------------------
250
254
  // Main
251
255
  // ---------------------------------------------------------------------------
252
- async function init() {
256
+ async function init(options = {}) {
257
+ const local = options.local ?? false;
258
+ const mcpPath = getMcpPath(local);
259
+ const scope = local ? `projeto (${process.cwd()})` : "global";
253
260
  console.log(`
254
261
  ╭─────────────────────────────────────╮
255
- │ LKPABAP.ia — Setup │
262
+ │ LKPABAP.ai — Setup │
256
263
  │ Conecte o Claude ao seu SAP │
257
264
  ╰─────────────────────────────────────╯
265
+
266
+ Escopo: ${scope}
258
267
  `);
259
- const config = readMcpConfig();
268
+ const config = readMcpConfig(mcpPath);
260
269
  const existingCount = Object.keys(config.mcpServers).filter((k) => k.startsWith("abap-")).length;
261
270
  if (existingCount > 0) {
262
271
  console.log(` ${existingCount} sistema(s) SAP já configurado(s).\n`);
@@ -320,10 +329,10 @@ async function init() {
320
329
  addMore = more;
321
330
  }
322
331
  if (addedSystems.length > 0) {
323
- writeMcpConfig(config);
332
+ writeMcpConfig(config, mcpPath);
324
333
  const total = Object.keys(config.mcpServers).filter((k) => k.startsWith("abap-")).length;
325
334
  console.log(`
326
- ✓ Configuração salva em ${MCP_JSON_PATH}
335
+ ✓ Configuração salva em ${mcpPath}
327
336
 
328
337
  ╭──────────────────────────────────────────────╮
329
338
  │ ${total} sistema(s) configurado(s):${" ".repeat(Math.max(0, 23 - total.toString().length))}│`);
@@ -40,16 +40,18 @@ exports.remove = remove;
40
40
  const prompts_1 = __importDefault(require("prompts"));
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
- const MCP_JSON_PATH = path.join(process.env.HOME || "~", ".claude", "mcp.json");
44
- async function remove(name) {
43
+ const GLOBAL_MCP_PATH = path.join(process.env.HOME || "~", ".claude", "mcp.json");
44
+ const LOCAL_MCP_PATH = path.join(process.cwd(), ".claude", "mcp.json");
45
+ async function remove(name, options = {}) {
45
46
  const serverName = name.startsWith("abap-") ? name : `abap-${name}`;
47
+ const mcpPath = options.local ? LOCAL_MCP_PATH : GLOBAL_MCP_PATH;
46
48
  let config;
47
49
  try {
48
- const raw = fs.readFileSync(MCP_JSON_PATH, "utf-8");
50
+ const raw = fs.readFileSync(mcpPath, "utf-8");
49
51
  config = JSON.parse(raw);
50
52
  }
51
53
  catch {
52
- console.error(` ✗ Arquivo ${MCP_JSON_PATH} não encontrado.`);
54
+ console.error(` ✗ Arquivo ${mcpPath} não encontrado.`);
53
55
  process.exit(1);
54
56
  return;
55
57
  }
@@ -73,8 +75,8 @@ async function remove(name) {
73
75
  return;
74
76
  }
75
77
  delete config.mcpServers[serverName];
76
- fs.writeFileSync(MCP_JSON_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
78
+ fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
77
79
  const remaining = Object.keys(config.mcpServers).filter((k) => k.startsWith("abap-")).length;
78
- console.log(`\n ✓ Sistema "${serverName}" removido de ${MCP_JSON_PATH}`);
80
+ console.log(`\n ✓ Sistema "${serverName}" removido de ${mcpPath}`);
79
81
  console.log(` ${remaining} sistema(s) restante(s).\n`);
80
82
  }
@@ -146,7 +146,7 @@ function countKnowledge() {
146
146
  async function status() {
147
147
  console.log(`
148
148
  ╭──────────────────────────────────────────────╮
149
- │ LKPABAP.ia v${VERSION}${" ".repeat(30 - VERSION.length)}│
149
+ │ LKPABAP.ai v${VERSION}${" ".repeat(30 - VERSION.length)}│
150
150
  ╰──────────────────────────────────────────────╯`);
151
151
  // Licença
152
152
  console.log(`
@@ -36,25 +36,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.systems = systems;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- const MCP_JSON_PATH = path.join(process.env.HOME || "~", ".claude", "mcp.json");
40
- async function systems() {
39
+ const GLOBAL_MCP_PATH = path.join(process.env.HOME || "~", ".claude", "mcp.json");
40
+ const LOCAL_MCP_PATH = path.join(process.cwd(), ".claude", "mcp.json");
41
+ async function systems(options = {}) {
42
+ const mcpPath = options.local ? LOCAL_MCP_PATH : GLOBAL_MCP_PATH;
41
43
  let config;
42
44
  try {
43
- const raw = fs.readFileSync(MCP_JSON_PATH, "utf-8");
45
+ const raw = fs.readFileSync(mcpPath, "utf-8");
44
46
  config = JSON.parse(raw);
45
47
  }
46
48
  catch {
47
49
  console.log("\n Nenhum sistema configurado.");
48
- console.log(" → Execute: abap-ai init\n");
50
+ console.log(` → Execute: abap-ai init${options.local ? " --local" : ""}\n`);
49
51
  return;
50
52
  }
51
53
  const sapServers = Object.entries(config.mcpServers || {}).filter(([k]) => k.startsWith("abap-"));
52
54
  if (sapServers.length === 0) {
53
55
  console.log("\n Nenhum sistema SAP configurado.");
54
- console.log(" → Execute: abap-ai init\n");
56
+ console.log(` → Execute: abap-ai init${options.local ? " --local" : ""}\n`);
55
57
  return;
56
58
  }
57
- console.log(`\n Sistemas SAP (${sapServers.length}):`);
59
+ const scope = options.local ? `projeto (${process.cwd()})` : "global";
60
+ console.log(`\n Sistemas SAP (${sapServers.length}) — ${scope}:`);
58
61
  console.log(" " + "─".repeat(60));
59
62
  for (const [name, server] of sapServers) {
60
63
  const env = server.env || {};
package/dist/cli.js CHANGED
@@ -6,29 +6,32 @@ const activate_js_1 = require("./cli/activate.js");
6
6
  const status_js_1 = require("./cli/status.js");
7
7
  const remove_js_1 = require("./cli/remove.js");
8
8
  const systems_js_1 = require("./cli/systems.js");
9
- const VERSION = "2.0.0";
9
+ const VERSION = "2.2.1";
10
10
  const HELP = `
11
- LKPABAP.ia — AI-powered ABAP development for SAP
11
+ LKPABAP.ai — AI-powered ABAP development for SAP
12
12
 
13
13
  Uso:
14
14
  abap-ai <comando> [opções]
15
15
 
16
16
  Comandos:
17
- init Configura conexão com sistema SAP (wizard interativo)
17
+ init [--local] Configura conexão com sistema SAP (wizard interativo)
18
18
  activate <LICENSE_KEY> Ativa licença do produto
19
19
  status Mostra licença, sistemas e métricas de uso
20
- systems Lista sistemas SAP configurados
21
- remove <nome> Remove um sistema SAP do mcp.json
20
+ systems [--local] Lista sistemas SAP configurados
21
+ remove <nome> [--local] Remove um sistema SAP do mcp.json
22
22
 
23
23
  Opções:
24
+ --local Usa config do projeto atual (.claude/mcp.json) em vez do global
24
25
  --version, -v Mostra versão
25
26
  --help, -h Mostra esta ajuda
26
27
 
27
28
  Exemplos:
28
- abap-ai init Wizard para adicionar sistema SAP
29
+ abap-ai init Wizard global (todos os workspaces)
30
+ abap-ai init --local Wizard para este workspace/projeto apenas
29
31
  abap-ai activate LK-XXXX Ativa licença
30
32
  abap-ai status Verifica estado do ambiente
31
- abap-ai remove novaforma-qas Remove sistema
33
+ abap-ai remove novaforma-qas Remove sistema (config global)
34
+ abap-ai remove rdg --local Remove sistema da config do projeto
32
35
  `;
33
36
  async function main() {
34
37
  const args = process.argv.slice(2);
@@ -38,12 +41,13 @@ async function main() {
38
41
  return;
39
42
  }
40
43
  if (command === "--version" || command === "-v") {
41
- console.log(`LKPABAP.ia v${VERSION}`);
44
+ console.log(`LKPABAP.ai v${VERSION}`);
42
45
  return;
43
46
  }
47
+ const local = args.includes("--local");
44
48
  switch (command) {
45
49
  case "init":
46
- await (0, init_js_1.init)();
50
+ await (0, init_js_1.init)({ local });
47
51
  break;
48
52
  case "activate": {
49
53
  const key = args[1];
@@ -58,15 +62,15 @@ async function main() {
58
62
  await (0, status_js_1.status)();
59
63
  break;
60
64
  case "systems":
61
- await (0, systems_js_1.systems)();
65
+ await (0, systems_js_1.systems)({ local });
62
66
  break;
63
67
  case "remove": {
64
- const name = args[1];
68
+ const name = args.filter((a) => !a.startsWith("--"))[1];
65
69
  if (!name) {
66
70
  console.error(" ✗ Informe o nome do sistema: abap-ai remove <nome>");
67
71
  process.exit(1);
68
72
  }
69
- await (0, remove_js_1.remove)(name);
73
+ await (0, remove_js_1.remove)(name, { local });
70
74
  break;
71
75
  }
72
76
  default:
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), TABL/DS (estrutura), TTYP/TT (table type)."),
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
- process.stderr.write(`abap-mcp-server v2.1.0 iniciado | System: ${profile.system.type} | ABAP: ${profile.system.abapPlatform} | Security: ${policy.environment_role} (${policy.allowed_levels.join("+")})\n`);
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
+ }
@@ -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.ia NUNCA sobrepõe as restrições de
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.ia. `
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.ia opera somente em modo leitura. "
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.ia opera somente em modo leitura. "
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
@@ -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) {
@@ -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
- // S/4HANA = BASIS 750+ com certas capabilities
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
- // Default para on-premise S/4
247
- return "ON_PREMISE_S4";
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;
@@ -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) => {
@@ -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.1.0",
4
- "description": "LKPABAP.ia — AI-powered ABAP development tools for SAP S/4HANA via ADT REST API",
3
+ "version": "2.2.1",
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"