@dozimple/abap-adt 1.0.1 → 1.1.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/CHANGELOG.md +40 -0
- package/README.es.md +7 -6
- package/README.md +7 -6
- package/dist/core/catalog.en.js +1 -0
- package/dist/core/catalog.js +1 -0
- package/dist/core/connection.js +41 -0
- package/dist/core/errors.js +14 -0
- package/dist/core/registry.js +80 -7
- package/dist/tools/core/revert_source.js +111 -0
- package/dist/tools/core/run_atc.js +9 -4
- package/dist/tools/core/run_unit_tests.js +4 -1
- package/dist/tools/core/sql_query.js +18 -2
- package/dist/tools/core/syntax_check.js +17 -2
- package/dist/tools/core/transport_diff.js +10 -1
- package/dist/tools/core/where_used.js +6 -1
- package/dist/tools/local/sap_systems.js +4 -0
- package/docs/TOOLS.md +44 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ Formato basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/). V
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.1.0] - 2026-09-22
|
|
8
|
+
|
|
9
|
+
Tool nueva y contrato MCP ampliado (salida estructurada, progreso, cancelación) más resiliencia de conexión: el sprint 1 y parte del 2 del plan de mejoras.
|
|
10
|
+
|
|
11
|
+
### Añadido
|
|
12
|
+
- **Progreso y cancelación** (sprint 2 del plan): las tools largas informan de su avance (`transport_diff` objeto a
|
|
13
|
+
objeto; `run_atc`, `where_used` y `run_unit_tests` por fase) y el cliente lo recibe como notificaciones de progreso
|
|
14
|
+
MCP si las pidió. La cancelación del cliente se respeta **entre pasos**: el paso en curso termina (una llamada ADT
|
|
15
|
+
no se puede abortar) y el siguiente ya no empieza. Una petición cancelada termina como `CANCELLED`, un tipo propio
|
|
16
|
+
que no cuenta como fallo del servidor ni abre el circuito.
|
|
17
|
+
- **Salida estructurada** (`outputSchema` / `structuredContent` de MCP), sprint 2 del plan: una tool puede declarar
|
|
18
|
+
`output` y devolver los mismos datos del texto de forma tipada, para que el cliente no tenga que interpretar la
|
|
19
|
+
prosa. El registro exige que toda respuesta no errónea de esas tools la traiga (si falta es error del servidor,
|
|
20
|
+
no un éxito a medias) y el SDK la valida contra el esquema antes de responder. Primeras tools: `sql_query` (filas,
|
|
21
|
+
columnas, valores hasta 500, `truncated`, avisos) y `syntax_check` (errores, avisos y mensajes con línea y
|
|
22
|
+
severidad). `docs/TOOLS.md` documenta el esquema de salida de cada una.
|
|
23
|
+
- **`revert_source`: volver a una versión anterior con confirmación.** Tras un `write_source` cuya activación falló,
|
|
24
|
+
el objeto queda con un borrador inactivo encima de la versión activa; la tool vuelve a escribir la versión elegida
|
|
25
|
+
(`active`: la última activa; `previous`: la anterior a la activa; `N`: una del historial de `object_versions`)
|
|
26
|
+
pasando por la misma vista previa, huella, bloqueo y orden que cualquier escritura. Nunca revierte por su cuenta:
|
|
27
|
+
un rollback automático que pisara una versión sin preguntar sería peor que dejar el objeto inactivo.
|
|
28
|
+
- **Resiliencia de conexión** (sprint 1 del plan de mejoras):
|
|
29
|
+
- **Sesión caducada renovada en las lecturas.** Si una tool de lectura recibe un token CSRF rechazado o un 401 en
|
|
30
|
+
una sesión que ya había entrado, el servidor descarta el cliente, vuelve a entrar y repite la lectura una vez;
|
|
31
|
+
la respuesta lo anota. Nunca en escrituras ni ejecuciones: el bloqueo se perdió con la sesión y un reintento
|
|
32
|
+
podría escribir dos veces. Una contraseña rechazada sigue fallando a la primera y sigue olvidándose.
|
|
33
|
+
- **Circuit breaker por sistema.** Tres fallos de red en un minuto abren el circuito de ESE sistema durante un
|
|
34
|
+
minuto: toda tool responde al instante «no se vuelve a intentar durante N s» en vez de esperar 120 s por llamada.
|
|
35
|
+
Los demás sistemas no se ven afectados; `sap_systems(check=true)` lo cierra y reintenta. Solo cuentan los fallos
|
|
36
|
+
de red reales de la librería, no los tiempos agotados propios.
|
|
37
|
+
- **Tiempo máximo por tool** (`timeoutMs`, por defecto 60 s; ATC y diff de orden 180 s, where-used y ABAP Unit
|
|
38
|
+
120 s). Al vencer, error `NETWORK` con el tiempo y la sugerencia de acotar; no se reintenta.
|
|
39
|
+
|
|
40
|
+
### Cambiado
|
|
41
|
+
- Insignia de **OpenSSF Best Practices (Passing)** en el README: el proyecto cumple los 67 criterios del nivel
|
|
42
|
+
Passing, incluidas las sugerencias, con la ficha pública en https://www.bestpractices.dev/projects/14759.
|
|
43
|
+
- El job de publicación usa Node 24, que ya trae npm >= 11.5.1: se quita la instalación global de npm, que no se
|
|
44
|
+
puede fijar por hash. Un paso comprueba la versión y falla antes de publicar si no la cumple. El build sigue en
|
|
45
|
+
Node 22, la versión mínima que soporta el servidor.
|
|
46
|
+
|
|
7
47
|
## [1.0.1] - 2026-09-22
|
|
8
48
|
|
|
9
49
|
Versión de mantenimiento: seguimiento de la auditoría de seguridad, procedimiento de commit y pruebas por propiedades.
|
package/README.es.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.md) · **Español**
|
|
6
6
|
|
|
7
|
-
[](../../actions/workflows/ci.yml)    
|
|
7
|
+
[](../../actions/workflows/ci.yml) [](https://www.bestpractices.dev/projects/14759)    
|
|
8
8
|
|
|
9
9
|
**IA que trabaja en tu SAP con las reglas de un consultor senior.**
|
|
10
10
|
|
|
@@ -61,7 +61,7 @@ El agente elige las tools, las encadena y responde con evidencia. Cada respuesta
|
|
|
61
61
|
| [Consulta de datos](#g-datos) | Preguntar a las tablas con ABAP SQL, de solo lectura y sin tocar material de credenciales. | 2 |
|
|
62
62
|
| [Diagnóstico de incidentes](#g-diagnostico) | Reunir en una conversación lo que antes exigía ST22, SM37, SLG1 y /IWFND/ERROR_LOG. | 4 |
|
|
63
63
|
| [Documentación SAP](#g-documentacion) | Responder con la documentación oficial y comprobar qué sintaxis existe en cada release. | 7 |
|
|
64
|
-
| [Escritura controlada](#g-escritura) | Guardar cambios solo en desarrollo, en la orden correcta y con la sintaxis verificada antes. |
|
|
64
|
+
| [Escritura controlada](#g-escritura) | Guardar cambios solo en desarrollo, en la orden correcta y con la sintaxis verificada antes. | 5 |
|
|
65
65
|
| [DoZimple Transport Risk](#g-transport-risk) | Decidir si un pase entero puede ir a calidad o productivo, con el porqué en lenguaje de negocio. | 7 |
|
|
66
66
|
| [Operación y crecimiento](#g-operacion) | Ver qué funciona en cada sistema y decidir con datos cuál es la siguiente tool. | 4 |
|
|
67
67
|
<!-- groups:end -->
|
|
@@ -187,6 +187,7 @@ la **[referencia completa](docs/TOOLS.md)**.
|
|
|
187
187
|
| Tool | Qué hace | Acceso |
|
|
188
188
|
|---|---|---|
|
|
189
189
|
| [`write_source`](docs/TOOLS.md#escritura) | **Guardar fuente en SAP.** Sustituye la fuente COMPLETA de un objeto existente (o de un include de clase), en la orden indicada. | escribe (DEV autorizado) |
|
|
190
|
+
| [`revert_source`](docs/TOOLS.md#escritura) | **Volver a una versión anterior.** Deshace un cambio escribiendo de nuevo una versión anterior del objeto: la última activa (para limpiar un borrador inactivo tras un write_source cuya activación falló), la anterior a la activa, o una concreta del historial de object_versions. | escribe (DEV autorizado) |
|
|
190
191
|
| [`activate`](docs/TOOLS.md#escritura) | **Activar objeto.** Activa un objeto y devuelve los mensajes de SAP tal cual (errores con línea, avisos, objetos que quedan inactivos). | escribe (DEV autorizado) |
|
|
191
192
|
| [`write_text_elements`](docs/TOOLS.md#escritura) | **Crear o cambiar símbolos de texto.** Añade o modifica símbolos de texto (o textos de selección) de un programa/clase/grupo, fusionando con los existentes: no borra los que no se mencionan. | escribe (DEV autorizado) |
|
|
192
193
|
| [`create_transport`](docs/TOOLS.md#escritura) | **Crear orden de transporte.** Crea una orden workbench para el paquete de un objeto, ANTES de la primera edición, para que el cambio caiga en la orden del ticket y no en una tarea reutilizada. | escribe (DEV autorizado) |
|
|
@@ -291,7 +292,7 @@ Modelo de amenazas con STRIDE y OWASP Top 10 para aplicaciones LLM: **[docs/THRE
|
|
|
291
292
|
Con la versión exacta y sin scripts de instalación, igual que las dependencias del propio proyecto:
|
|
292
293
|
|
|
293
294
|
```sh
|
|
294
|
-
npm install -g --ignore-scripts @dozimple/abap-adt@1.0
|
|
295
|
+
npm install -g --ignore-scripts @dozimple/abap-adt@1.1.0
|
|
295
296
|
PKG="$(npm root -g)/@dozimple/abap-adt"
|
|
296
297
|
mkdir -p ~/.config/abap-adt-dozimple && chmod 700 ~/.config/abap-adt-dozimple
|
|
297
298
|
cp "$PKG/config/systems.example.json" ~/.config/abap-adt-dozimple/systems.json # sistemas, roles y permisos
|
|
@@ -307,9 +308,9 @@ Registro en el cliente MCP:
|
|
|
307
308
|
```
|
|
308
309
|
|
|
309
310
|
Cada versión se publica desde el CI con [procedencia de npm](https://docs.npmjs.com/generating-provenance-statements):
|
|
310
|
-
`npm view @dozimple/abap-adt@1.0
|
|
311
|
+
`npm view @dozimple/abap-adt@1.1.0 dist.attestations` muestra la atestación, y la release de GitHub incluye el
|
|
311
312
|
paquete, su bundle de Sigstore (`.sigstore.json`), el mismo bundle como procedencia in-toto (`.intoto.jsonl`)
|
|
312
|
-
y el SBOM. Para comprobarlo: `gh attestation verify dozimple-abap-adt-1.0.
|
|
313
|
+
y el SBOM. Para comprobarlo: `gh attestation verify dozimple-abap-adt-1.1.0.tgz --repo <owner>/abap-adt-dozimple`.
|
|
313
314
|
|
|
314
315
|
### Desde el código fuente
|
|
315
316
|
|
|
@@ -343,7 +344,7 @@ abap-adt-doZimple se construye sobre el trabajo de otros, y lo reconoce: cada to
|
|
|
343
344
|
<!-- credits:start -->
|
|
344
345
|
| Proyecto | Autor / titular | Licencia | Tipo | Usado en |
|
|
345
346
|
|---|---|---|---|---|
|
|
346
|
-
| [abap-adt-api](https://github.com/marcellourbani/abap-adt-api) | Marcello Urbani | MIT | dependencia | todas (núcleo), `transport_diff`, `transport_contents`, `inactive_objects`, `edit_preflight`, `run_atc` y
|
|
347
|
+
| [abap-adt-api](https://github.com/marcellourbani/abap-adt-api) | Marcello Urbani | MIT | dependencia | todas (núcleo), `transport_diff`, `transport_contents`, `inactive_objects`, `edit_preflight`, `run_atc` y 23 más |
|
|
347
348
|
| [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) | Model Context Protocol | MIT | dependencia | todas (núcleo) |
|
|
348
349
|
| [mcp-sap-docs](https://github.com/marianfoo/mcp-sap-docs) | Marian Zeis (marianfoo) | Apache-2.0 | dependencia | `abap_feature_matrix`, `docs_search`, `docs_fetch`, `clean_core_objects`, `clean_core_object`, `abap_lint` y 1 más |
|
|
349
350
|
| [abaplint](https://github.com/abaplint/abaplint) | Lars Hvam y contribuidores | MIT | dependencia | `abap_lint` |
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**English** · [Español](README.es.md)
|
|
6
6
|
|
|
7
|
-
[](../../actions/workflows/ci.yml)    
|
|
7
|
+
[](../../actions/workflows/ci.yml) [](https://www.bestpractices.dev/projects/14759)    
|
|
8
8
|
|
|
9
9
|
**AI that works on your SAP system with the rules of a senior consultant.**
|
|
10
10
|
|
|
@@ -62,7 +62,7 @@ and **a failure is never presented as an empty result or as success**.
|
|
|
62
62
|
| [Data queries](#g-datos) | Query tables with ABAP SQL, read-only, with sensitive and personal data protected. | 2 |
|
|
63
63
|
| [Incident diagnosis](#g-diagnostico) | One conversation for what used to take ST22, SM37, SLG1 and /IWFND/ERROR_LOG. | 4 |
|
|
64
64
|
| [SAP documentation](#g-documentacion) | Answer from official documentation and check which syntax exists in each release. | 7 |
|
|
65
|
-
| [Controlled writes](#g-escritura) | Save changes only in development, in the right transport, previewed and confirmed by a human. |
|
|
65
|
+
| [Controlled writes](#g-escritura) | Save changes only in development, in the right transport, previewed and confirmed by a human. | 5 |
|
|
66
66
|
| [DoZimple Transport Risk](#g-transport-risk) | Decide whether a whole release can go to QA or production, with the why in business terms. | 7 |
|
|
67
67
|
| [Operations and growth](#g-operacion) | See what works on each system and decide the next tool with data. | 4 |
|
|
68
68
|
<!-- groups:end -->
|
|
@@ -188,6 +188,7 @@ Summary per group; each tool's details — parameters, types, defaults, requirem
|
|
|
188
188
|
| Tool | What it does | Access |
|
|
189
189
|
|---|---|---|
|
|
190
190
|
| [`write_source`](docs/TOOLS.md#escritura) | **Save source to SAP.** Replaces the FULL source of an existing object (or class include) in the given transport, after a preview with syntax check and diff and a human confirmation. | writes (authorized DEV) |
|
|
191
|
+
| [`revert_source`](docs/TOOLS.md#escritura) | **Revert to an earlier version.** Writes back an earlier version of an object (the last active one, the one before it, or a numbered one from object_versions) through the same preview, fingerprint, lock and transport as write_source; never reverts on its own. | writes (authorized DEV) |
|
|
191
192
|
| [`activate`](docs/TOOLS.md#escritura) | **Activate object.** Activates an object and returns SAP's messages as they are (errors with line, warnings, objects left inactive). | writes (authorized DEV) |
|
|
192
193
|
| [`write_text_elements`](docs/TOOLS.md#escritura) | **Create or change text symbols.** Adds or changes text symbols (or selection texts) of a program/class/group, merging with the existing ones: nothing not mentioned is deleted. | writes (authorized DEV) |
|
|
193
194
|
| [`create_transport`](docs/TOOLS.md#escritura) | **Create transport request.** Creates a workbench request for an object's package BEFORE the first edit, so the change lands in the ticket's transport and not in a reused task. | writes (authorized DEV) |
|
|
@@ -291,7 +292,7 @@ Designed to pass a Security and Basis review without exceptions. Details: **[SEC
|
|
|
291
292
|
Pinned to an exact version, without install scripts, like the project's own dependencies:
|
|
292
293
|
|
|
293
294
|
```sh
|
|
294
|
-
npm install -g --ignore-scripts @dozimple/abap-adt@1.0
|
|
295
|
+
npm install -g --ignore-scripts @dozimple/abap-adt@1.1.0
|
|
295
296
|
PKG="$(npm root -g)/@dozimple/abap-adt"
|
|
296
297
|
mkdir -p ~/.config/abap-adt-dozimple && chmod 700 ~/.config/abap-adt-dozimple
|
|
297
298
|
cp "$PKG/config/systems.example.json" ~/.config/abap-adt-dozimple/systems.json # systems, roles and permissions
|
|
@@ -307,9 +308,9 @@ MCP client registration:
|
|
|
307
308
|
```
|
|
308
309
|
|
|
309
310
|
Every release is published from CI with [npm provenance](https://docs.npmjs.com/generating-provenance-statements):
|
|
310
|
-
`npm view @dozimple/abap-adt@1.0
|
|
311
|
+
`npm view @dozimple/abap-adt@1.1.0 dist.attestations` shows the attestation, and the GitHub release carries the
|
|
311
312
|
tarball, its Sigstore bundle (`.sigstore.json`), the same bundle as in-toto provenance (`.intoto.jsonl`) and the
|
|
312
|
-
SBOM. To check it: `gh attestation verify dozimple-abap-adt-1.0.
|
|
313
|
+
SBOM. To check it: `gh attestation verify dozimple-abap-adt-1.1.0.tgz --repo <owner>/abap-adt-dozimple`.
|
|
313
314
|
|
|
314
315
|
### From source
|
|
315
316
|
|
|
@@ -344,7 +345,7 @@ abap-adt-doZimple is built on other people's work, and says so: each tool lists
|
|
|
344
345
|
<!-- credits:start -->
|
|
345
346
|
| Project | Author / holder | License | Type | Used in |
|
|
346
347
|
|---|---|---|---|---|
|
|
347
|
-
| [abap-adt-api](https://github.com/marcellourbani/abap-adt-api) | Marcello Urbani | MIT | dependency | all (core), `transport_diff`, `transport_contents`, `inactive_objects`, `edit_preflight`, `run_atc` and
|
|
348
|
+
| [abap-adt-api](https://github.com/marcellourbani/abap-adt-api) | Marcello Urbani | MIT | dependency | all (core), `transport_diff`, `transport_contents`, `inactive_objects`, `edit_preflight`, `run_atc` and 23 more |
|
|
348
349
|
| [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) | Model Context Protocol | MIT | dependency | all (core) |
|
|
349
350
|
| [mcp-sap-docs](https://github.com/marianfoo/mcp-sap-docs) | Marian Zeis (marianfoo) | Apache-2.0 | dependency | `abap_feature_matrix`, `docs_search`, `docs_fetch`, `clean_core_objects`, `clean_core_object`, `abap_lint` and 1 more |
|
|
350
351
|
| [abaplint](https://github.com/abaplint/abaplint) | Lars Hvam and contributors | MIT | dependency | `abap_lint` |
|
package/dist/core/catalog.en.js
CHANGED
|
@@ -48,6 +48,7 @@ export const TOOLS_EN = {
|
|
|
48
48
|
abap_lint: "**abaplint on a snippet.** Runs abaplint locally (code never leaves the machine) on an ABAP snippet or source.",
|
|
49
49
|
docs_community_search: "**Search SAP Community.** Searches SAP Community (blogs and questions) by error message, class or concept.",
|
|
50
50
|
write_source: "**Save source to SAP.** Replaces the FULL source of an existing object (or class include) in the given transport, after a preview with syntax check and diff and a human confirmation.",
|
|
51
|
+
revert_source: "**Revert to an earlier version.** Writes back an earlier version of an object (the last active one, the one before it, or a numbered one from object_versions) through the same preview, fingerprint, lock and transport as write_source; never reverts on its own.",
|
|
51
52
|
activate: "**Activate object.** Activates an object and returns SAP's messages as they are (errors with line, warnings, objects left inactive).",
|
|
52
53
|
write_text_elements: "**Create or change text symbols.** Adds or changes text symbols (or selection texts) of a program/class/group, merging with the existing ones: nothing not mentioned is deleted.",
|
|
53
54
|
create_transport: "**Create transport request.** Creates a workbench request for an object's package BEFORE the first edit, so the change lands in the ticket's transport and not in a reused task.",
|
package/dist/core/catalog.js
CHANGED
|
@@ -100,6 +100,7 @@ export const GROUPS = [
|
|
|
100
100
|
pitch: "Guardar cambios solo en desarrollo, en la orden correcta y con la sintaxis verificada antes.",
|
|
101
101
|
tools: [
|
|
102
102
|
{ name: "write_source", credits: ADT },
|
|
103
|
+
{ name: "revert_source", credits: ADT },
|
|
103
104
|
{ name: "activate", credits: ADT },
|
|
104
105
|
{ name: "write_text_elements", credits: ADT },
|
|
105
106
|
{ name: "create_transport", credits: ADT },
|
package/dist/core/connection.js
CHANGED
|
@@ -8,6 +8,10 @@ import { normalizeValue, wrapSql } from "./sql.js";
|
|
|
8
8
|
const CACHE_DIR = join(homedir(), ".cache", "abap-adt-dozimple");
|
|
9
9
|
const DISCOVERY_TTL_MS = 7 * 24 * 3600 * 1000;
|
|
10
10
|
const REQUEST_TIMEOUT_MS = 120_000;
|
|
11
|
+
/** Circuit breaker: N fallos de red en la ventana abren el circuito durante CIRCUIT_OPEN_MS. */
|
|
12
|
+
const CIRCUIT_FAILURES = 3;
|
|
13
|
+
const CIRCUIT_WINDOW_MS = 60_000;
|
|
14
|
+
const CIRCUIT_OPEN_MS = 60_000;
|
|
11
15
|
/**
|
|
12
16
|
* TLS: con caFile se verifica contra ese certificado (lo correcto para un
|
|
13
17
|
* sistema con certificado propio); allowSelfSigned desactiva la verificación
|
|
@@ -30,9 +34,44 @@ export class SapConnection {
|
|
|
30
34
|
reader;
|
|
31
35
|
caps;
|
|
32
36
|
basisRelease;
|
|
37
|
+
networkFailures = [];
|
|
38
|
+
openUntil = 0;
|
|
33
39
|
constructor(system) {
|
|
34
40
|
this.system = system;
|
|
35
41
|
}
|
|
42
|
+
/** Olvida el cliente de lectura (sesión caducada): la siguiente llamada vuelve a entrar. */
|
|
43
|
+
resetReader() {
|
|
44
|
+
const old = this.reader;
|
|
45
|
+
this.reader = undefined;
|
|
46
|
+
old?.then((c) => c.logout()).catch(() => undefined);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Tres fallos de red en un minuto abren el circuito: durante un minuto ninguna tool intenta nada contra este
|
|
50
|
+
* sistema y responde al instante. Un sistema caído (VPN, host) dejaba de responder 120 s por cada llamada.
|
|
51
|
+
*/
|
|
52
|
+
noteNetworkFailure(now = Date.now()) {
|
|
53
|
+
this.networkFailures = this.networkFailures.filter((t) => now - t < CIRCUIT_WINDOW_MS);
|
|
54
|
+
this.networkFailures.push(now);
|
|
55
|
+
if (this.networkFailures.length >= CIRCUIT_FAILURES) {
|
|
56
|
+
this.openUntil = now + CIRCUIT_OPEN_MS;
|
|
57
|
+
this.networkFailures = [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Segundos que faltan para volver a intentar, o 0 si el circuito está cerrado. */
|
|
61
|
+
circuitOpenFor(now = Date.now()) {
|
|
62
|
+
return this.openUntil > now ? Math.ceil((this.openUntil - now) / 1000) : 0;
|
|
63
|
+
}
|
|
64
|
+
/** Cierra el circuito a mano (sap_systems con check): se vuelve a intentar ya. */
|
|
65
|
+
resetCircuit() {
|
|
66
|
+
this.openUntil = 0;
|
|
67
|
+
this.networkFailures = [];
|
|
68
|
+
}
|
|
69
|
+
assertCircuitClosed() {
|
|
70
|
+
const s = this.circuitOpenFor();
|
|
71
|
+
if (s) {
|
|
72
|
+
throw new ToolError("NETWORK", `${this.system.id} no respondió ${CIRCUIT_FAILURES} veces seguidas: no se vuelve a intentar durante ${s} s.`, "sap_systems(check=true) lo reintenta ahora mismo.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
36
75
|
async newClient() {
|
|
37
76
|
const s = this.system;
|
|
38
77
|
const password = await getPassword(s);
|
|
@@ -53,6 +92,7 @@ export class SapConnection {
|
|
|
53
92
|
}
|
|
54
93
|
/** Cliente de lectura, reutilizado entre llamadas. */
|
|
55
94
|
async adt() {
|
|
95
|
+
this.assertCircuitClosed();
|
|
56
96
|
if (!this.reader) {
|
|
57
97
|
this.reader = this.newClient().then((c) => this.login(c));
|
|
58
98
|
this.reader.catch(() => (this.reader = undefined));
|
|
@@ -61,6 +101,7 @@ export class SapConnection {
|
|
|
61
101
|
}
|
|
62
102
|
/** Ejecuta fn con una sesión stateful propia y la cierra siempre. */
|
|
63
103
|
async stateful(fn) {
|
|
104
|
+
this.assertCircuitClosed();
|
|
64
105
|
const c = await this.login(await this.newClient());
|
|
65
106
|
c.stateful = session_types.stateful;
|
|
66
107
|
try {
|
package/dist/core/errors.js
CHANGED
|
@@ -19,6 +19,9 @@ export function normalizeError(e, systemId) {
|
|
|
19
19
|
if (e instanceof ToolError)
|
|
20
20
|
return e;
|
|
21
21
|
const any = e;
|
|
22
|
+
// signal.throwIfAborted() entre pasos de una tool larga: cancelación del cliente, no fallo del servidor.
|
|
23
|
+
if (any?.name === "AbortError")
|
|
24
|
+
return new ToolError("CANCELLED", "Cancelado por el cliente antes de terminar.");
|
|
22
25
|
const code = errCode(any);
|
|
23
26
|
if (code && NETWORK_CODES.has(code)) {
|
|
24
27
|
return new ToolError("NETWORK", `No se llega a ${systemId ?? "SAP"} (${code}).`, "¿Está levantada la VPN de ese cliente?");
|
|
@@ -50,6 +53,16 @@ export function normalizeError(e, systemId) {
|
|
|
50
53
|
const message = sanitizeMessage(any?.message ? String(any.message) : String(e));
|
|
51
54
|
return new ToolError("INTERNAL", message || "Error sin mensaje");
|
|
52
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Sesión caducada a mitad de una lectura: token CSRF rechazado, o 401 en una sesión que ya había entrado. El registro
|
|
58
|
+
* la renueva y repite la lectura UNA vez; nunca una escritura (el bloqueo se perdió con la sesión).
|
|
59
|
+
*/
|
|
60
|
+
export function isSessionExpired(e) {
|
|
61
|
+
if (isCsrfError(e))
|
|
62
|
+
return true;
|
|
63
|
+
const st = e;
|
|
64
|
+
return (isHttpError(e) && st?.status === 401) || (isAdtError(e) && st?.err === 401);
|
|
65
|
+
}
|
|
53
66
|
export function renderError(te) {
|
|
54
67
|
const labels = {
|
|
55
68
|
NETWORK: "Sin conexión",
|
|
@@ -60,6 +73,7 @@ export function renderError(te) {
|
|
|
60
73
|
CAPABILITY: "No disponible en este sistema",
|
|
61
74
|
MODULE: "Módulo no habilitado",
|
|
62
75
|
INPUT: "Parámetros inválidos",
|
|
76
|
+
CANCELLED: "Cancelado",
|
|
63
77
|
INTERNAL: "Error interno",
|
|
64
78
|
};
|
|
65
79
|
return `${labels[te.kind]}: ${te.message}${te.hint ? `\n${te.hint}` : ""}\n` +
|
package/dist/core/registry.js
CHANGED
|
@@ -3,11 +3,11 @@ import { join } from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { canWrite, resolveSystem } from "./config.js";
|
|
6
|
-
import { normalizeError, renderError, ToolError } from "./errors.js";
|
|
6
|
+
import { isSessionExpired, normalizeError, renderError, ToolError } from "./errors.js";
|
|
7
7
|
import { budget } from "./output.js";
|
|
8
8
|
import { appendAudit, auditArgs } from "./audit.js";
|
|
9
9
|
import { consumeTokenWithState, issueToken } from "./confirm.js";
|
|
10
|
-
import { renderNotes, withNotes } from "./notes.js";
|
|
10
|
+
import { addNote, renderNotes, withNotes } from "./notes.js";
|
|
11
11
|
import { assertAccess } from "./policy.js";
|
|
12
12
|
import { recordUsage } from "./telemetry.js";
|
|
13
13
|
/**
|
|
@@ -182,6 +182,15 @@ export async function invoke(def, rawArgs, env) {
|
|
|
182
182
|
throw new ToolError("INTERNAL", "No hay componentes auxiliares en este servidor.");
|
|
183
183
|
return env.sidecars.get(name);
|
|
184
184
|
},
|
|
185
|
+
signal: env.signal,
|
|
186
|
+
progress(message, current, total) {
|
|
187
|
+
try {
|
|
188
|
+
env.progress?.(message, current, total);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* el progreso es cortesía: un fallo al notificar nunca rompe la tool */
|
|
192
|
+
}
|
|
193
|
+
},
|
|
185
194
|
};
|
|
186
195
|
const head = resolved ? `Sistema: ${resolved.system.id} (${resolved.source})\n${SAP_DATA_NOTE}\n\n` : "";
|
|
187
196
|
if (resolved && (def.access === "write" || def.access === "exec")) {
|
|
@@ -218,14 +227,23 @@ export async function invoke(def, rawArgs, env) {
|
|
|
218
227
|
throw new ToolError("INTERNAL", `No se ejecutó: no se pudo escribir el registro de auditoría (${e.message}).`);
|
|
219
228
|
}
|
|
220
229
|
}
|
|
221
|
-
const { result: res, notes } = await withNotes(() => def
|
|
230
|
+
const { result: res, notes } = await withNotes(() => runGuarded(def, args, ctx, sapConn, systemId));
|
|
222
231
|
const text = renderNotes(notes) + (typeof res === "string" ? res : res.text);
|
|
223
232
|
const isError = typeof res === "string" ? false : !!res.isError;
|
|
224
|
-
|
|
233
|
+
const structured = typeof res === "string" ? undefined : res.structured;
|
|
234
|
+
// Una tool con esquema de salida que responde bien sin datos estructurados es un bug: mejor verlo aquí que
|
|
235
|
+
// como error críptico del SDK en el cliente.
|
|
236
|
+
if (def.output && !isError && !structured)
|
|
237
|
+
throw new ToolError("INTERNAL", `${def.name} declara salida estructurada y no la devolvió.`);
|
|
238
|
+
outcome = { text: head + budget(text), isError, kind: isError ? "RESULT" : undefined, system: systemId, structured };
|
|
225
239
|
}
|
|
226
240
|
}
|
|
227
241
|
catch (e) {
|
|
228
242
|
let te = normalizeError(e, systemId);
|
|
243
|
+
// Solo los fallos de red REALES (de la librería) cuentan para el circuito: ni un timeout nuestro ni el propio
|
|
244
|
+
// aviso de circuito abierto.
|
|
245
|
+
if (te.kind === "NETWORK" && !(e instanceof ToolError) && sapConn)
|
|
246
|
+
sapConn.noteNetworkFailure();
|
|
229
247
|
// Un 404 de SAP (no un «objeto no existe» nuestro) sobre un endpoint que el
|
|
230
248
|
// discovery tampoco lista: ahora sí hay evidencia de que falta la función.
|
|
231
249
|
if (te.kind === "NOT_FOUND" && !(e instanceof ToolError) && missing.length && sapConn) {
|
|
@@ -251,6 +269,50 @@ export async function invoke(def, rawArgs, env) {
|
|
|
251
269
|
});
|
|
252
270
|
return outcome;
|
|
253
271
|
}
|
|
272
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
273
|
+
function withTimeout(p, ms, what, signal) {
|
|
274
|
+
let timer;
|
|
275
|
+
let onAbort;
|
|
276
|
+
const limit = new Promise((_, reject) => {
|
|
277
|
+
if (signal) {
|
|
278
|
+
onAbort = () => reject(new ToolError("CANCELLED", `${what}: cancelado por el cliente.`));
|
|
279
|
+
if (signal.aborted)
|
|
280
|
+
onAbort();
|
|
281
|
+
else
|
|
282
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
283
|
+
}
|
|
284
|
+
timer = setTimeout(() => reject(new ToolError("NETWORK", `${what}: tiempo agotado (${ms >= 1000 ? `${Math.round(ms / 1000)} s` : `${ms} ms`}).`, "Acota la petición (menos objetos, más filtro) o repite más tarde.")), ms);
|
|
285
|
+
});
|
|
286
|
+
return Promise.race([p, limit]).finally(() => {
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
if (signal && onAbort)
|
|
289
|
+
signal.removeEventListener("abort", onAbort);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Ejecuta la tool con su tiempo máximo y, si la sesión de lectura había caducado (CSRF o 401 tras haber entrado),
|
|
294
|
+
* la renueva y repite UNA vez. Nunca repite escrituras ni ejecuciones: el bloqueo se perdió con la sesión y un
|
|
295
|
+
* reintento podría escribir dos veces.
|
|
296
|
+
*/
|
|
297
|
+
async function runGuarded(def, args, ctx, sapConn, systemId) {
|
|
298
|
+
const ms = def.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
299
|
+
const attempt = () => {
|
|
300
|
+
if (ctx.signal?.aborted)
|
|
301
|
+
throw new ToolError("CANCELLED", `${def.name}: cancelado por el cliente.`);
|
|
302
|
+
return withTimeout(def.run(args, ctx), ms, def.name, ctx.signal);
|
|
303
|
+
};
|
|
304
|
+
try {
|
|
305
|
+
return await attempt();
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
if (def.access === "read" && sapConn && isSessionExpired(e)) {
|
|
309
|
+
sapConn.resetReader();
|
|
310
|
+
addNote(`La sesión con ${systemId ?? "SAP"} había caducado: se renovó y la lectura se repitió.`);
|
|
311
|
+
return await attempt();
|
|
312
|
+
}
|
|
313
|
+
throw e;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
254
316
|
/** Elicitación de formulario, si el cliente la anuncia (ABAP_DZ_CONFIRM=token la desactiva). */
|
|
255
317
|
function elicitFor(server) {
|
|
256
318
|
const caps = server.server.getClientCapabilities()?.elicitation;
|
|
@@ -313,10 +375,21 @@ export function registerAll(server, defs, config, pool, sidecars) {
|
|
|
313
375
|
title: def.title,
|
|
314
376
|
description: describe(def, config),
|
|
315
377
|
inputSchema,
|
|
378
|
+
...(def.output ? { outputSchema: z.object(def.output) } : {}),
|
|
316
379
|
annotations: annotationsFor(def),
|
|
317
|
-
}, (async (args) => {
|
|
318
|
-
const
|
|
319
|
-
|
|
380
|
+
}, (async (args, extra) => {
|
|
381
|
+
const token = extra?._meta?.progressToken;
|
|
382
|
+
const progress = token !== undefined && extra?.sendNotification
|
|
383
|
+
? (message, current, total) => {
|
|
384
|
+
void extra.sendNotification({ method: "notifications/progress", params: { progressToken: token, progress: current ?? 0, total, message } }).catch(() => undefined);
|
|
385
|
+
}
|
|
386
|
+
: undefined;
|
|
387
|
+
const r = await invoke(def, args ?? {}, { config, pool, tools: defs, sidecars, elicit: elicitFor(server), signal: extra?.signal, progress });
|
|
388
|
+
return {
|
|
389
|
+
content: [{ type: "text", text: r.text }],
|
|
390
|
+
isError: r.isError,
|
|
391
|
+
...(r.structured ? { structuredContent: r.structured } : {}),
|
|
392
|
+
};
|
|
320
393
|
}));
|
|
321
394
|
published.push(def.name);
|
|
322
395
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isError, renderSyntax, syntaxCheck } from "../../core/checks.js";
|
|
3
|
+
import { stateOf } from "../../core/confirm.js";
|
|
4
|
+
import { diffLines, unified } from "../../core/diff.js";
|
|
5
|
+
import { ToolError } from "../../core/errors.js";
|
|
6
|
+
import { CLASS_INCLUDES, resolveObject, sourceUrl, TYPE_HELP } from "../../core/objects.js";
|
|
7
|
+
import { assertTrkorr } from "../../core/policy.js";
|
|
8
|
+
import { versionNumber } from "../../core/revisions.js";
|
|
9
|
+
import { defineTool } from "../../core/tool.js";
|
|
10
|
+
import { transportWarnings } from "../../core/transport.js";
|
|
11
|
+
import writeSource from "./write_source.js";
|
|
12
|
+
/**
|
|
13
|
+
* Deshacer con confirmación, nunca automático. Tras un write_source cuya activación falló, el objeto queda con un
|
|
14
|
+
* borrador inactivo (versión 99999 de ADT) encima de la versión activa (00000). Esta tool escribe de nuevo la
|
|
15
|
+
* versión elegida —la activa, la anterior a la activa, o una concreta del historial— pasando por la misma vista
|
|
16
|
+
* previa, confirmación, bloqueo y orden que cualquier escritura. Un rollback que pisara una versión sin preguntar
|
|
17
|
+
* sería peor que dejar el objeto inactivo.
|
|
18
|
+
*/
|
|
19
|
+
const TARGET_HELP = "«active»: la última versión activa (deshace un borrador inactivo, p. ej. un write_source cuya activación falló). " +
|
|
20
|
+
"«previous»: la versión anterior a la activa (deshace la última activación). " +
|
|
21
|
+
"Un número N: la versión N tal como la lista object_versions.";
|
|
22
|
+
function pickRevision(revs, target) {
|
|
23
|
+
if (typeof target === "number") {
|
|
24
|
+
const rev = revs[target - 1];
|
|
25
|
+
if (!rev)
|
|
26
|
+
throw new ToolError("INPUT", `Solo hay ${revs.length} versiones (object_versions las lista).`);
|
|
27
|
+
return { rev, label: `versión ${target} del historial` };
|
|
28
|
+
}
|
|
29
|
+
const active = revs.find((r) => versionNumber(r) === "00000");
|
|
30
|
+
if (target === "active") {
|
|
31
|
+
if (!active)
|
|
32
|
+
throw new ToolError("NOT_FOUND", "SAP no devuelve una versión activa de este objeto.");
|
|
33
|
+
return { rev: active, label: "última versión activa" };
|
|
34
|
+
}
|
|
35
|
+
const numbered = revs
|
|
36
|
+
.filter((r) => /^\d{5}$/.test(versionNumber(r)) && !["00000", "99999"].includes(versionNumber(r)))
|
|
37
|
+
.sort((a, b) => (Date.parse(b.date) || 0) - (Date.parse(a.date) || 0) || Number(versionNumber(b)) - Number(versionNumber(a)));
|
|
38
|
+
if (!numbered.length)
|
|
39
|
+
throw new ToolError("NOT_FOUND", "No hay ninguna versión grabada anterior a la activa.");
|
|
40
|
+
return { rev: numbered[0], label: "versión anterior a la activa" };
|
|
41
|
+
}
|
|
42
|
+
async function chosenSource(c, obj, include, target) {
|
|
43
|
+
const revs = await c.revisions(obj.uri, obj.type.startsWith("CLAS") ? include : undefined);
|
|
44
|
+
if (!revs.length)
|
|
45
|
+
throw new ToolError("NOT_FOUND", `${obj.name}: SAP no devuelve versiones para este objeto.`);
|
|
46
|
+
const { rev, label } = pickRevision(revs, target);
|
|
47
|
+
const hasDraft = revs.some((r) => versionNumber(r) === "99999");
|
|
48
|
+
return { rev, label, hasDraft, source: await c.getObjectSource(rev.uri) };
|
|
49
|
+
}
|
|
50
|
+
const input = {
|
|
51
|
+
object_name: z.string().min(1),
|
|
52
|
+
object_type: z.string().optional().describe(TYPE_HELP),
|
|
53
|
+
include: z.enum(CLASS_INCLUDES).default("main"),
|
|
54
|
+
target: z.union([z.enum(["active", "previous"]), z.number().int().min(1)]).default("active").describe(TARGET_HELP),
|
|
55
|
+
transport: z.string().optional().describe("Orden (o tarea) donde debe ir la reversión. Obligatoria salvo objetos locales"),
|
|
56
|
+
activate: z.boolean().default(true),
|
|
57
|
+
};
|
|
58
|
+
export default defineTool({
|
|
59
|
+
name: "revert_source",
|
|
60
|
+
title: "Volver a una versión anterior",
|
|
61
|
+
description: "Deshace un cambio escribiendo de nuevo una versión anterior del objeto: la última activa (para limpiar un " +
|
|
62
|
+
"borrador inactivo tras un write_source cuya activación falló), la anterior a la activa, o una concreta del " +
|
|
63
|
+
"historial de object_versions. Pasa por la misma vista previa, confirmación, bloqueo y orden que write_source: " +
|
|
64
|
+
"nunca revierte por su cuenta. Solo objetos de código fuente.",
|
|
65
|
+
access: "write",
|
|
66
|
+
input,
|
|
67
|
+
async preview({ object_name, object_type, include, target, transport, activate }, { sap, system }) {
|
|
68
|
+
const requested = transport ? assertTrkorr(transport) : undefined;
|
|
69
|
+
const c = await sap.adt();
|
|
70
|
+
const obj = await resolveObject(c, object_name, object_type);
|
|
71
|
+
const url = await sourceUrl(c, obj, include);
|
|
72
|
+
const current = await c.getObjectSource(url);
|
|
73
|
+
const { rev, label, hasDraft, source } = await chosenSource(c, obj, include, target);
|
|
74
|
+
const out = [
|
|
75
|
+
`${obj.name} (${obj.type})${include !== "main" ? ` · include ${include}` : ""} · paquete ${obj.packageName ?? "?"}`,
|
|
76
|
+
`Volver a: ${label} · ${rev.date} · ${rev.author}${rev.versionTitle ? ` · ${rev.versionTitle}` : ""}`,
|
|
77
|
+
`Orden: ${requested ?? "ninguna indicada (solo vale para objetos locales)"} · activar después: ${activate ? "sí" : "no"}`,
|
|
78
|
+
hasDraft ? "Estado: el objeto tiene un borrador inactivo (cambios guardados sin activar)." : "Estado: sin borrador inactivo.",
|
|
79
|
+
];
|
|
80
|
+
if (target === "active" && !hasDraft)
|
|
81
|
+
out.push("Aviso: no hay borrador que deshacer; la versión activa ya es la que se ve.");
|
|
82
|
+
if (requested) {
|
|
83
|
+
const warn = await transportWarnings(sap, requested, system.user);
|
|
84
|
+
out.push(warn.length ? `⚠ AVISOS DE LA ORDEN ${requested}:\n${warn.map((w) => " ⚠ " + w).join("\n")}` : `Orden ${requested}: modificable, con destino y del usuario de esta conexión.`);
|
|
85
|
+
}
|
|
86
|
+
const msgs = await syntaxCheck(c, obj, url, source);
|
|
87
|
+
out.push(msgs.some(isError) ? `Sintaxis de la versión elegida: CON ERRORES, la escritura se detendrá.\n${renderSyntax(msgs)}` : `Sintaxis de la versión elegida: sin errores${msgs.length ? ` (${msgs.length} avisos)` : ""}.`);
|
|
88
|
+
const ops = diffLines(current, source);
|
|
89
|
+
if (!ops)
|
|
90
|
+
out.push("", "Cambio: el objeto se reescribe casi entero (demasiadas diferencias para mostrarlas como diff).");
|
|
91
|
+
else {
|
|
92
|
+
const u = unified(ops, 3);
|
|
93
|
+
const lines = u.text.split("\n");
|
|
94
|
+
out.push("", u.hunks ? `Cambio respecto a lo que hay ahora: +${u.added} −${u.removed} en ${u.hunks} bloques` : "Cambio: ninguno (la versión elegida es idéntica a lo que hay ahora).");
|
|
95
|
+
if (u.hunks)
|
|
96
|
+
out.push(lines.slice(0, 400).join("\n") + (lines.length > 400 ? `\n[… ${lines.length - 400} líneas más de diff]` : ""));
|
|
97
|
+
}
|
|
98
|
+
return { text: out.join("\n"), state: stateOf(current) };
|
|
99
|
+
},
|
|
100
|
+
async run({ object_name, object_type, include, target, transport, activate }, ctx) {
|
|
101
|
+
const c = await ctx.sap.adt();
|
|
102
|
+
const obj = await resolveObject(c, object_name, object_type);
|
|
103
|
+
const { label, source } = await chosenSource(c, obj, include, target);
|
|
104
|
+
// La escritura real es la de write_source: mismo bloqueo, misma comprobación de que nada cambió desde la vista
|
|
105
|
+
// previa (ctx.confirmedState), misma decisión de orden y misma activación.
|
|
106
|
+
const r = await writeSource.run({ object_name, object_type, include, source, transport, activate, skip_syntax_check: false }, ctx);
|
|
107
|
+
const text = typeof r === "string" ? r : r.text;
|
|
108
|
+
return { text: `Revertido a la ${label}.\n${text}`, isError: typeof r === "string" ? false : !!r.isError };
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
//# sourceMappingURL=revert_source.js.map
|
|
@@ -10,9 +10,10 @@ import { defineTool } from "../../core/tool.js";
|
|
|
10
10
|
const decode = decodeEntities;
|
|
11
11
|
const strip = htmlToText;
|
|
12
12
|
/** Ejecuta el ATC del alcance y lo recuerda bajo ese alcance. */
|
|
13
|
-
async function execute(c, sap, systemId, t, o) {
|
|
13
|
+
async function execute(c, sap, systemId, t, o, ctx) {
|
|
14
14
|
const variant = o.variant ?? (await defaultVariant(c));
|
|
15
15
|
let res;
|
|
16
|
+
ctx?.progress?.(`ATC de ${t.scope} con la variante ${variant}…`);
|
|
16
17
|
try {
|
|
17
18
|
res = await runAtc(c, t.uri, variant, o.max_findings, o.include_exempted);
|
|
18
19
|
res.scope = t.scope;
|
|
@@ -35,6 +36,8 @@ async function execute(c, sap, systemId, t, o) {
|
|
|
35
36
|
const uris = (await Promise.all(refs.map((r) => resolveByTypePrefix(c, r.name, r.types)))).filter(Boolean).map((r) => r.uri);
|
|
36
37
|
if (!uris.length)
|
|
37
38
|
throw new ToolError("NOT_FOUND", `La orden ${tr} no contiene objetos que el ATC pueda revisar.`);
|
|
39
|
+
ctx?.signal?.throwIfAborted();
|
|
40
|
+
ctx?.progress?.(`Este release no admite la orden como conjunto: ATC sobre sus ${uris.length} objetos en una corrida…`);
|
|
38
41
|
res = await runAtc(c, uris, variant, o.max_findings, o.include_exempted);
|
|
39
42
|
res.scope = `orden ${tr} (sus ${uris.length} objetos: este release no admite la orden como conjunto ATC)`;
|
|
40
43
|
}
|
|
@@ -44,6 +47,7 @@ async function execute(c, sap, systemId, t, o) {
|
|
|
44
47
|
}
|
|
45
48
|
export default defineTool({
|
|
46
49
|
name: "run_atc",
|
|
50
|
+
timeoutMs: 180_000,
|
|
47
51
|
title: "Ejecutar ATC",
|
|
48
52
|
description: "Ejecuta el ATC sobre un objeto o una orden de transporte y lista los hallazgos numerados (prioridad, línea, " +
|
|
49
53
|
"check, mensaje), con los totales P1/P2/P3 que da SAP. El resultado queda recordado POR OBJETO U ORDEN: " +
|
|
@@ -66,7 +70,8 @@ export default defineTool({
|
|
|
66
70
|
.optional()
|
|
67
71
|
.describe("Documentación del hallazgo N del ATC del objeto u orden indicados (sin ellos: del último ATC, y lo dice)"),
|
|
68
72
|
},
|
|
69
|
-
async run(a,
|
|
73
|
+
async run(a, ctx) {
|
|
74
|
+
const { sap, system } = ctx;
|
|
70
75
|
const c = await sap.adt();
|
|
71
76
|
// El alcance (objeto u orden nombrados) se resuelve antes que nada: explain=N es SIEMPRE de ese alcance.
|
|
72
77
|
let target;
|
|
@@ -83,7 +88,7 @@ export default defineTool({
|
|
|
83
88
|
let run = target ? recallRun(system.id, target.key) : recallRun(system.id);
|
|
84
89
|
let ranNow = false;
|
|
85
90
|
if (!run && target) {
|
|
86
|
-
run = await execute(c, sap, system.id, target, opts);
|
|
91
|
+
run = await execute(c, sap, system.id, target, opts, ctx);
|
|
87
92
|
ranNow = true;
|
|
88
93
|
}
|
|
89
94
|
if (!run)
|
|
@@ -99,7 +104,7 @@ export default defineTool({
|
|
|
99
104
|
}
|
|
100
105
|
if (!target)
|
|
101
106
|
throw new ToolError("INPUT", "Indica object_name o transport.");
|
|
102
|
-
const res = await execute(c, sap, system.id, target, opts);
|
|
107
|
+
const res = await execute(c, sap, system.id, target, opts, ctx);
|
|
103
108
|
const shown = res.findings.filter((f) => !a.priorities || a.priorities.includes(f.priority));
|
|
104
109
|
const byPrio = [1, 2, 3, 4].map((p) => res.findings.filter((f) => f.priority === p).length);
|
|
105
110
|
const stats = res.stats ? `SAP: P1 ${res.stats.p1} · P2 ${res.stats.p2} · P3 ${res.stats.p3}` : `P1 ${byPrio[0]} · P2 ${byPrio[1]} · P3 ${byPrio[2]}`;
|
|
@@ -9,6 +9,7 @@ import { defineTool } from "../../core/tool.js";
|
|
|
9
9
|
export const SAFE_TEST_FLAGS = { harmless: true, dangerous: false, critical: false, short: true, medium: false, long: false };
|
|
10
10
|
export default defineTool({
|
|
11
11
|
name: "run_unit_tests",
|
|
12
|
+
timeoutMs: 120_000,
|
|
12
13
|
title: "Ejecutar ABAP Unit",
|
|
13
14
|
description: "Ejecuta los tests ABAP Unit de una clase o programa y devuelve el resultado por método, con el detalle de cada " +
|
|
14
15
|
"fallo. Solo corre tests RISK LEVEL HARMLESS y DURATION SHORT. Si no hay clases de test lo dice: cero tests no es un éxito.",
|
|
@@ -20,9 +21,11 @@ export default defineTool({
|
|
|
20
21
|
object_name: z.string().min(1),
|
|
21
22
|
object_type: z.string().default("CLAS").describe(TYPE_HELP),
|
|
22
23
|
},
|
|
23
|
-
async run({ object_name, object_type },
|
|
24
|
+
async run({ object_name, object_type }, ctx) {
|
|
25
|
+
const { sap } = ctx;
|
|
24
26
|
const c = await sap.adt();
|
|
25
27
|
const obj = await resolveObject(c, object_name, object_type);
|
|
28
|
+
ctx.progress?.(`Ejecutando ABAP Unit de ${obj.name} (solo tests harmless y short)…`);
|
|
26
29
|
const classes = await c.unitTestRun(obj.uri, { ...SAFE_TEST_FLAGS });
|
|
27
30
|
if (!classes.length)
|
|
28
31
|
return { text: `${obj.name}: no se encontraron clases de test. No se ejecutó nada.`, isError: true };
|
|
@@ -2,6 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import { guardedQuery } from "../../core/datapolicy.js";
|
|
3
3
|
import { tsv } from "../../core/output.js";
|
|
4
4
|
import { defineTool } from "../../core/tool.js";
|
|
5
|
+
/** La salida estructurada duplica el contenido del texto: se acota para no doblar respuestas enormes. */
|
|
6
|
+
const STRUCTURED_ROWS = 500;
|
|
5
7
|
export default defineTool({
|
|
6
8
|
name: "sql_query",
|
|
7
9
|
title: "Consulta ABAP SQL",
|
|
@@ -14,12 +16,26 @@ export default defineTool({
|
|
|
14
16
|
query: z.string().min(1).describe("SELECT ... FROM ... WHERE ..."),
|
|
15
17
|
max_rows: z.number().int().min(1).max(5000).default(100),
|
|
16
18
|
},
|
|
19
|
+
output: {
|
|
20
|
+
rows: z.number().int().describe("Filas devueltas"),
|
|
21
|
+
columns: z.array(z.string()),
|
|
22
|
+
values: z.array(z.record(z.unknown())).describe(`Filas como objetos columna→valor (hasta ${STRUCTURED_ROWS}; el texto las trae todas)`),
|
|
23
|
+
truncated: z.boolean().describe("true si values no incluye todas las filas"),
|
|
24
|
+
notes: z.array(z.string()).describe("Avisos de la política de datos (enmascarado, tope de filas)"),
|
|
25
|
+
},
|
|
17
26
|
async run({ query, max_rows }, { sap, system }) {
|
|
18
27
|
const r = await guardedQuery(sap, system, query, max_rows);
|
|
28
|
+
const structured = {
|
|
29
|
+
rows: r.values.length,
|
|
30
|
+
columns: r.columns,
|
|
31
|
+
values: r.values.slice(0, STRUCTURED_ROWS),
|
|
32
|
+
truncated: r.values.length > STRUCTURED_ROWS,
|
|
33
|
+
notes: r.notes,
|
|
34
|
+
};
|
|
19
35
|
if (!r.values.length)
|
|
20
|
-
return `La consulta se ejecutó y no devolvió filas
|
|
36
|
+
return { text: `La consulta se ejecutó y no devolvió filas.`, structured };
|
|
21
37
|
const table = tsv(r.columns, r.values.map((v) => r.columns.map((c) => v[c])));
|
|
22
|
-
return `${r.values.length} filas\n\n${table}${r.notes.length ? `\n\n${r.notes.join("\n")}` : ""}
|
|
38
|
+
return { text: `${r.values.length} filas\n\n${table}${r.notes.length ? `\n\n${r.notes.join("\n")}` : ""}`, structured };
|
|
23
39
|
},
|
|
24
40
|
});
|
|
25
41
|
//# sourceMappingURL=sql_query.js.map
|
|
@@ -16,6 +16,13 @@ export default defineTool({
|
|
|
16
16
|
source: z.string().optional().describe("Fuente completa a comprobar (sin guardar)"),
|
|
17
17
|
main_program: z.string().optional().describe("URI del programa principal, solo para includes ambiguos"),
|
|
18
18
|
},
|
|
19
|
+
output: {
|
|
20
|
+
object: z.string(),
|
|
21
|
+
checked: z.enum(["proposed", "saved"]).describe("proposed = la fuente pasada sin guardar; saved = lo último guardado"),
|
|
22
|
+
errors: z.number().int(),
|
|
23
|
+
warnings: z.number().int(),
|
|
24
|
+
messages: z.array(z.object({ severity: z.string(), line: z.number(), offset: z.number(), text: z.string(), uri: z.string() })),
|
|
25
|
+
},
|
|
19
26
|
async run({ object_name, object_type, include, source, main_program }, { sap }) {
|
|
20
27
|
const c = await sap.adt();
|
|
21
28
|
const obj = await resolveObject(c, object_name, object_type);
|
|
@@ -23,12 +30,20 @@ export default defineTool({
|
|
|
23
30
|
const content = source ?? (await c.getObjectSource(url, { version: "inactive" }));
|
|
24
31
|
const msgs = await syntaxCheck(c, obj, url, content, main_program);
|
|
25
32
|
const what = source ? "fuente propuesta (no guardada)" : "última versión guardada";
|
|
26
|
-
if (!msgs.length)
|
|
27
|
-
return `${obj.name}: sin errores ni avisos de sintaxis (${what}, comprobado por SAP).`;
|
|
28
33
|
const errs = msgs.filter(isError).length;
|
|
34
|
+
const structured = {
|
|
35
|
+
object: obj.name,
|
|
36
|
+
checked: source ? "proposed" : "saved",
|
|
37
|
+
errors: errs,
|
|
38
|
+
warnings: msgs.length - errs,
|
|
39
|
+
messages: msgs.map((m) => ({ severity: m.severity ?? "", line: m.line, offset: m.offset, text: m.text, uri: m.uri })),
|
|
40
|
+
};
|
|
41
|
+
if (!msgs.length)
|
|
42
|
+
return { text: `${obj.name}: sin errores ni avisos de sintaxis (${what}, comprobado por SAP).`, structured };
|
|
29
43
|
return {
|
|
30
44
|
text: `${obj.name}: ${errs} errores, ${msgs.length - errs} avisos (${what}).\n\n${renderSyntax(msgs)}`,
|
|
31
45
|
isError: errs > 0,
|
|
46
|
+
structured,
|
|
32
47
|
};
|
|
33
48
|
},
|
|
34
49
|
});
|
|
@@ -22,6 +22,7 @@ async function pool(items, n, fn) {
|
|
|
22
22
|
}
|
|
23
23
|
export default defineTool({
|
|
24
24
|
name: "transport_diff",
|
|
25
|
+
timeoutMs: 180_000,
|
|
25
26
|
title: "Qué cambió una orden (diff de código)",
|
|
26
27
|
description: "Revisión de código de una orden: por cada objeto con fuente (programas, includes, clases, interfaces, FM, CDS) " +
|
|
27
28
|
"compara la versión grabada con esa orden (o sus tareas) contra la versión anterior, y muestra el diff unificado. " +
|
|
@@ -36,7 +37,8 @@ export default defineTool({
|
|
|
36
37
|
max_objects: z.number().int().min(1).max(60).default(20),
|
|
37
38
|
max_diff_lines: z.number().int().min(20).max(3000).default(300).describe("Tope de líneas de diff por objeto"),
|
|
38
39
|
},
|
|
39
|
-
async run({ transport, objects, context, summary_only, max_objects, max_diff_lines },
|
|
40
|
+
async run({ transport, objects, context, summary_only, max_objects, max_diff_lines }, ctx) {
|
|
41
|
+
const { sap } = ctx;
|
|
40
42
|
const tr = assertTrkorr(transport);
|
|
41
43
|
const heads = await orderHeaders(sap, [tr]);
|
|
42
44
|
const head = heads.get(tr);
|
|
@@ -51,7 +53,11 @@ export default defineTool({
|
|
|
51
53
|
const selected = source.filter((s) => !wanted || wanted.includes(s.name.toUpperCase()));
|
|
52
54
|
const shown = selected.slice(0, max_objects);
|
|
53
55
|
const c = await sap.adt();
|
|
56
|
+
let done = 0;
|
|
57
|
+
ctx.progress?.(`Comparando ${shown.length} objetos de ${root}…`, 0, shown.length);
|
|
54
58
|
const blocks = await pool(shown, 4, async (ref) => {
|
|
59
|
+
// Cancelación entre objetos: el que está en curso termina, el siguiente ya no empieza.
|
|
60
|
+
ctx.signal?.throwIfAborted();
|
|
55
61
|
try {
|
|
56
62
|
const res = await resolveRef(c, ref);
|
|
57
63
|
if (!res)
|
|
@@ -91,6 +97,9 @@ export default defineTool({
|
|
|
91
97
|
throw te;
|
|
92
98
|
return { name: ref.name, text: `■ ${ref.name}: no se pudo comparar (${te.kind}: ${te.message})`, added: 0, removed: 0 };
|
|
93
99
|
}
|
|
100
|
+
finally {
|
|
101
|
+
ctx.progress?.(`${++done} de ${shown.length} objetos comparados`, done, shown.length);
|
|
102
|
+
}
|
|
94
103
|
});
|
|
95
104
|
const tot = blocks.reduce((a, b) => ({ added: a.added + b.added, removed: a.removed + b.removed }), { added: 0, removed: 0 });
|
|
96
105
|
const out = [
|
|
@@ -4,6 +4,7 @@ import { tsv } from "../../core/output.js";
|
|
|
4
4
|
import { defineTool } from "../../core/tool.js";
|
|
5
5
|
export default defineTool({
|
|
6
6
|
name: "where_used",
|
|
7
|
+
timeoutMs: 120_000,
|
|
7
8
|
title: "Dónde se usa",
|
|
8
9
|
description: "Lista de uso (where-used) de un objeto: quién lo referencia, con paquete y responsable. Con snippets=true añade " +
|
|
9
10
|
"las líneas de código de cada uso (más lento). Ojo: no ve usos dinámicos ni exits que no declaran tipos.",
|
|
@@ -15,10 +16,13 @@ export default defineTool({
|
|
|
15
16
|
max_results: z.number().int().min(1).max(1000).default(100),
|
|
16
17
|
snippets: z.boolean().default(false),
|
|
17
18
|
},
|
|
18
|
-
async run({ object_name, object_type, max_results, snippets },
|
|
19
|
+
async run({ object_name, object_type, max_results, snippets }, ctx) {
|
|
20
|
+
const { sap } = ctx;
|
|
19
21
|
const c = await sap.adt();
|
|
20
22
|
const obj = await resolveObject(c, object_name, object_type);
|
|
23
|
+
ctx.progress?.(`Buscando usos de ${obj.name} (en objetos muy usados tarda hasta un minuto)…`);
|
|
21
24
|
const refs = (await c.usageReferences(obj.uri)).filter((r) => r.isResult);
|
|
25
|
+
ctx.signal?.throwIfAborted();
|
|
22
26
|
if (!refs.length)
|
|
23
27
|
return `${obj.name} (${obj.type}): el where-used se ejecutó y no encontró usos estáticos.`;
|
|
24
28
|
const shown = refs.slice(0, max_results);
|
|
@@ -27,6 +31,7 @@ export default defineTool({
|
|
|
27
31
|
"\n\n" +
|
|
28
32
|
tsv(["nombre", "tipo", "paquete", "responsable", "uso"], shown.map((r) => [r["adtcore:name"], r["adtcore:type"], r.packageRef?.["adtcore:name"], r["adtcore:responsible"], r.usageInformation]));
|
|
29
33
|
if (snippets) {
|
|
34
|
+
ctx.progress?.(`${refs.length} usos; leyendo fragmentos de los primeros ${Math.min(shown.length, 30)}…`);
|
|
30
35
|
const sn = await c.usageReferenceSnippets(shown.slice(0, 30));
|
|
31
36
|
const lines = sn.flatMap((s) => s.snippets.map((x) => `${s.objectIdentifier} L${x.uri?.start?.line ?? "?"}: ${x.content.trim()}`));
|
|
32
37
|
out += `\n\nFragmentos (hasta 30 objetos):\n${lines.join("\n")}`;
|
|
@@ -30,6 +30,10 @@ export default defineTool({
|
|
|
30
30
|
if (!check)
|
|
31
31
|
continue;
|
|
32
32
|
const sap = pool.get(s);
|
|
33
|
+
if (sap.circuitOpenFor()) {
|
|
34
|
+
out.push(" circuito abierto por fallos de red seguidos: se cierra y se reintenta ahora");
|
|
35
|
+
sap.resetCircuit();
|
|
36
|
+
}
|
|
33
37
|
try {
|
|
34
38
|
const caps = await sap.capabilities(refresh_discovery);
|
|
35
39
|
const rel = await sap.release();
|
package/docs/TOOLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Referencia de tools — abap-adt-doZimple
|
|
2
2
|
|
|
3
|
-
Generado desde el código con `npm run docs`.
|
|
3
|
+
Generado desde el código con `npm run docs`. 48 tools en 9 grupos y 3 flujos guiados.
|
|
4
4
|
Producto de [DoZimple](https://dozimple.cl).
|
|
5
5
|
|
|
6
6
|
## Índice
|
|
@@ -11,7 +11,7 @@ Producto de [DoZimple](https://dozimple.cl).
|
|
|
11
11
|
- [Consulta de datos](#datos) — 2 tools: `sql_query`, `table_contents`
|
|
12
12
|
- [Diagnóstico de incidentes](#diagnostico) — 4 tools: `dumps`, `jobs`, `application_log`, `gateway_errors`
|
|
13
13
|
- [Documentación SAP](#documentacion) — 7 tools: `abap_feature_matrix`, `docs_search`, `docs_fetch`, `clean_core_objects`, `clean_core_object`, `abap_lint`, `docs_community_search`
|
|
14
|
-
- [Escritura controlada](#escritura) —
|
|
14
|
+
- [Escritura controlada](#escritura) — 5 tools: `write_source`, `revert_source`, `activate`, `write_text_elements`, `create_transport`
|
|
15
15
|
- [DoZimple Transport Risk](#transport-risk) — 7 tools: `analyze_transport_risk`, `import_health`, `failure_ranking`, `change_audit`, `object_transport_history`, `remote_source`, `transport_source_check`
|
|
16
16
|
- [Operación y crecimiento](#operacion) — 4 tools: `sap_systems`, `report_gap`, `close_gap`, `usage_stats`
|
|
17
17
|
- [Flujos guiados](#flujos-guiados)
|
|
@@ -206,6 +206,16 @@ Chequeo de sintaxis real de SAP (no abaplint). Si pasas `source`, se comprueba E
|
|
|
206
206
|
|
|
207
207
|
\* obligatorio
|
|
208
208
|
|
|
209
|
+
Salida estructurada (`structuredContent`, además del texto):
|
|
210
|
+
|
|
211
|
+
| Campo | Tipo | Descripción |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| `object` | string | |
|
|
214
|
+
| `checked` | `proposed` \| `saved` | proposed = la fuente pasada sin guardar; saved = lo último guardado |
|
|
215
|
+
| `errors` | number | |
|
|
216
|
+
| `warnings` | number | |
|
|
217
|
+
| `messages` | lista de { severity, line, offset, text, uri } | |
|
|
218
|
+
|
|
209
219
|
### `run_unit_tests` — Ejecutar ABAP Unit
|
|
210
220
|
|
|
211
221
|
Ejecuta los tests ABAP Unit de una clase o programa y devuelve el resultado por método, con el detalle de cada fallo. Solo corre tests RISK LEVEL HARMLESS y DURATION SHORT. Si no hay clases de test lo dice: cero tests no es un éxito.
|
|
@@ -417,6 +427,16 @@ Ejecuta un SELECT de ABAP SQL (con WHERE, JOIN, ORDER BY, subconsultas) vía la
|
|
|
417
427
|
|
|
418
428
|
\* obligatorio
|
|
419
429
|
|
|
430
|
+
Salida estructurada (`structuredContent`, además del texto):
|
|
431
|
+
|
|
432
|
+
| Campo | Tipo | Descripción |
|
|
433
|
+
|---|---|---|
|
|
434
|
+
| `rows` | number | Filas devueltas |
|
|
435
|
+
| `columns` | lista de string | |
|
|
436
|
+
| `values` | lista de record | Filas como objetos columna→valor (hasta 500; el texto las trae todas) |
|
|
437
|
+
| `truncated` | boolean | true si values no incluye todas las filas |
|
|
438
|
+
| `notes` | lista de string | Avisos de la política de datos (enmascarado, tope de filas) |
|
|
439
|
+
|
|
420
440
|
### `table_contents` — Contenido de una tabla
|
|
421
441
|
|
|
422
442
|
Filas de una tabla, vista o CDS, con columnas y filtro opcionales. Atajo de sql_query para el caso típico «enséñame lo que hay en ZTABLA donde …». Para JOIN, subconsultas o agregados usa sql_query.
|
|
@@ -684,6 +704,28 @@ Sustituye la fuente COMPLETA de un objeto existente (o de un include de clase),
|
|
|
684
704
|
|
|
685
705
|
\* obligatorio
|
|
686
706
|
|
|
707
|
+
### `revert_source` — Volver a una versión anterior
|
|
708
|
+
|
|
709
|
+
Deshace un cambio escribiendo de nuevo una versión anterior del objeto: la última activa (para limpiar un borrador inactivo tras un write_source cuya activación falló), la anterior a la activa, o una concreta del historial de object_versions. Pasa por la misma vista previa, confirmación, bloqueo y orden que write_source: nunca revierte por su cuenta. Solo objetos de código fuente.
|
|
710
|
+
|
|
711
|
+
| | |
|
|
712
|
+
|---|---|
|
|
713
|
+
| **Acceso** | Escribe (solo DEV con `allowWrite`; nunca QAS/PRD) |
|
|
714
|
+
| **Créditos** | [abap-adt-api](https://github.com/marcellourbani/abap-adt-api) — Marcello Urbani (MIT, dependencia) |
|
|
715
|
+
|
|
716
|
+
| Parámetro | Tipo | Por defecto | Descripción |
|
|
717
|
+
|---|---|---|---|
|
|
718
|
+
| `system` | string | | Sistema SAP configurado. Obligatorio si hay varios y ninguno por defecto. |
|
|
719
|
+
| `object_name` * | string | | |
|
|
720
|
+
| `object_type` | string | | Tipo corto: PROG, INCL, CLAS, INTF, FUGR, FUNC, DDLS, DDLX, DCLS, TABL, STRU, VIEW, DTEL, DOMA, TTYP, MSAG, XSLT, BDEF, SRVD, SRVB, ENHO. También vale el tipo ADT (p. ej. PROG/P). |
|
|
721
|
+
| `include` | `main` \| `definitions` \| `implementations` \| `macros` \| `testclasses` | "main" | |
|
|
722
|
+
| `target` | union | "active" | «active»: la última versión activa (deshace un borrador inactivo, p. ej. un write_source cuya activación falló). «previous»: la versión anterior a la activa (deshace la última activación). Un número N: la versión N tal como la lista object_versions. |
|
|
723
|
+
| `transport` | string | | Orden (o tarea) donde debe ir la reversión. Obligatoria salvo objetos locales |
|
|
724
|
+
| `activate` | boolean | true | |
|
|
725
|
+
| `confirm_token` | string | | Token de la vista previa. Sin él la tool no escribe: devuelve qué va a cambiar y el token, que se usa tras la conformidad del usuario (un solo uso, 10 min). |
|
|
726
|
+
|
|
727
|
+
\* obligatorio
|
|
728
|
+
|
|
687
729
|
### `activate` — Activar objeto
|
|
688
730
|
|
|
689
731
|
Activa un objeto y devuelve los mensajes de SAP tal cual (errores con línea, avisos, objetos que quedan inactivos).
|
package/package.json
CHANGED