@cspeach/cli 1.1.38 → 1.1.40
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/agent/loop.js +17 -2
- package/dist/agent/tool-dispatch.js +11 -0
- package/dist/sap/profile-features.js +42 -0
- package/dist/sap/profile-gate.js +67 -0
- package/dist/sap/system-info.js +1 -1
- package/dist/sap/system-profile.js +312 -0
- package/dist/tools/rap/finish-check.js +1 -0
- package/dist/tools/rap/generate-objects.js +1 -0
- package/dist/tools/sap-read.js +29 -2
- package/dist/tools/sap-write.js +10 -0
- package/dist/tools/text-elements/tools.js +2 -0
- package/package.json +2 -2
package/dist/agent/loop.js
CHANGED
|
@@ -57,6 +57,8 @@ import { expandTextFileAttachments } from '../projects/workspace.js';
|
|
|
57
57
|
import { expandImageAttachments, buildUserContent, trimHistoryImages } from '../projects/image-attachments.js';
|
|
58
58
|
import { expandImageChips } from '../ui/clipboard-image.js';
|
|
59
59
|
import { getSapSystemInfo, renderSapSystemBlock } from '../sap/system-info.js';
|
|
60
|
+
import { getSystemProfile, renderSapProfileBlock } from '../sap/system-profile.js';
|
|
61
|
+
import { pinHiddenTools, withoutTools } from '../sap/profile-gate.js';
|
|
60
62
|
import { renderStandaloneContextBlock } from '../sap/standalone-profile.js';
|
|
61
63
|
import { renderStandardsBlock } from '../standards/standards-file.js';
|
|
62
64
|
import { composeSessionContextBlock } from './session-context.js';
|
|
@@ -820,6 +822,7 @@ ${imageExpansion.error}`));
|
|
|
820
822
|
// SAP block is then empty and the rest of enrichment proceeds as
|
|
821
823
|
// before — no behavioral regression.
|
|
822
824
|
let sapSystemBlock = '';
|
|
825
|
+
let sapProfileBlock = '';
|
|
823
826
|
let sapSystemInfo = null;
|
|
824
827
|
const standaloneTurn = isStandalone(params.ctx);
|
|
825
828
|
if (standaloneTurn) {
|
|
@@ -828,10 +831,15 @@ ${imageExpansion.error}`));
|
|
|
828
831
|
// so skills read one element in both modes — plus the manual-
|
|
829
832
|
// implementation hand-off note.
|
|
830
833
|
sapSystemBlock = renderStandaloneContextBlock(cfg.standalone);
|
|
834
|
+
params.ctx.systemProfile = null;
|
|
831
835
|
}
|
|
832
836
|
else if (params.ctx.adt && params.ctx.sapAlias) {
|
|
833
837
|
sapSystemInfo = await getSapSystemInfo(params.ctx.adt, params.ctx.sapAlias);
|
|
838
|
+
// ADT gap program, Part 1 — the system profile rides the same lazy probe.
|
|
839
|
+
// Null (kill switch, no discovery and no CVERS) = today's behaviour.
|
|
840
|
+
params.ctx.systemProfile = await getSystemProfile(params.ctx.adt, params.ctx.sapAlias, sapSystemInfo);
|
|
834
841
|
sapSystemBlock = renderSapSystemBlock(sapSystemInfo);
|
|
842
|
+
sapProfileBlock = renderSapProfileBlock(params.ctx.systemProfile);
|
|
835
843
|
}
|
|
836
844
|
// Customer development standards — opt-in, both modes. Renders '' when the
|
|
837
845
|
// user has no standards file, so the composed block (and therefore the whole
|
|
@@ -842,7 +850,7 @@ ${imageExpansion.error}`));
|
|
|
842
850
|
const switchNote = session.systemSwitchNote
|
|
843
851
|
? `<system_switch>${session.systemSwitchNote}</system_switch>`
|
|
844
852
|
: '';
|
|
845
|
-
const sessionContextBlock = composeSessionContextBlock([sapSystemBlock, standardsBlock, switchNote]);
|
|
853
|
+
const sessionContextBlock = composeSessionContextBlock([sapSystemBlock, sapProfileBlock, standardsBlock, switchNote]);
|
|
846
854
|
// Deterministically extract structured facts from the user's prompt
|
|
847
855
|
// (package, transport, object names, environment hint, create intent)
|
|
848
856
|
// and prepend them as an XML `<session_context>` block. The model reads
|
|
@@ -926,7 +934,14 @@ ${imageExpansion.error}`));
|
|
|
926
934
|
env: process.env,
|
|
927
935
|
served: servedToolLoading(),
|
|
928
936
|
});
|
|
929
|
-
|
|
937
|
+
// Part 1 (D4, R7) — the hidden set is pinned per alias on the first
|
|
938
|
+
// connected turn and reused, so tools[] stays byte-stable; [] pinned = same
|
|
939
|
+
// array as today. A standalone or unconnected turn pins nothing.
|
|
940
|
+
const connectedTurn = !standaloneTurn && !!params.ctx.adt && !!params.ctx.sapAlias;
|
|
941
|
+
const hiddenSapTools = connectedTurn
|
|
942
|
+
? pinHiddenTools(session, params.ctx.sapAlias, listTools(), params.ctx.systemProfile ?? null)
|
|
943
|
+
: [];
|
|
944
|
+
const contextTools = withoutTools(toolsForContext(listTools(), params.ctx), hiddenSapTools);
|
|
930
945
|
// Task 18 fixes M4 / M5 — no read_agent for an agent_run child, and none
|
|
931
946
|
// when a reader would have no real read tool (standalone, file tools off).
|
|
932
947
|
const visibleTools = params.ctx.subagent || !hasReaderReadTools(new Set(contextTools.map((t) => t.name)))
|
|
@@ -9,6 +9,7 @@ import { shouldGateRule8, recordWriteOp, renderPlanSummary, getWriteOpsThisTurn,
|
|
|
9
9
|
import { isCredentialHandoverApproved, markCredentialHandoverApproved, } from '../repl/credential-handover-gate.js';
|
|
10
10
|
import { READER_TOOLS } from '../tools/subagent/reader-prompt.js';
|
|
11
11
|
import { riskOf } from './risk-gate.js';
|
|
12
|
+
import { NOT_ON_THIS_RELEASE, profileGate } from '../sap/profile-gate.js';
|
|
12
13
|
const READER_TOOL_SET = new Set(READER_TOOLS);
|
|
13
14
|
export async function dispatchTool(name, args, ctx) {
|
|
14
15
|
const tool = getTool(name);
|
|
@@ -36,6 +37,16 @@ export async function dispatchTool(name, args, ctx) {
|
|
|
36
37
|
is_error: true,
|
|
37
38
|
};
|
|
38
39
|
}
|
|
40
|
+
// ADT gap program, Part 1 (D4) — a definite "not on this release" is
|
|
41
|
+
// answered here, before any SAP call, as a normal tool error. Unknown or
|
|
42
|
+
// no profile: today's path, untouched.
|
|
43
|
+
const gate = profileGate(tool, args, ctx.systemProfile ?? null);
|
|
44
|
+
if (gate) {
|
|
45
|
+
return {
|
|
46
|
+
content: JSON.stringify({ error: NOT_ON_THIS_RELEASE, feature: gate.feature, message: gate.message }),
|
|
47
|
+
is_error: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
39
50
|
// L1-6 R1-6 (owner decision 2026-09-21) — a call that hands the stored SAP
|
|
40
51
|
// login to a local child process ALWAYS asks a human, once per session per
|
|
41
52
|
// alias, even as the first mutating call of a turn (where Rule 8 below stays
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const A = '/sap/bc/adt';
|
|
2
|
+
export const FEATURES = Object.freeze({
|
|
3
|
+
'cds.ddls': { label: 'CDS data definitions', collection: `${A}/ddic/ddl/sources`, since: { basis: 740, sp: 5, label: 'ABAP 7.40 SP05' }, provenOnOurBox: true },
|
|
4
|
+
'cds.viewEntity': { label: 'CDS view entities (define view entity)', since: { basis: 755, label: 'S/4HANA 2020 (ABAP 7.55)' }, provenOnOurBox: true },
|
|
5
|
+
'cds.tableEntity': { label: 'CDS table entities (define table entity)', since: { basis: 816, label: 'S/4HANA 2025 (ABAP 8.16)' }, provenOnOurBox: false },
|
|
6
|
+
'cds.ddlx': { label: 'CDS metadata extensions', collection: `${A}/ddic/ddlx/sources`, since: { basis: 751, label: 'ABAP 7.51' }, provenOnOurBox: true },
|
|
7
|
+
'cds.dcls': { label: 'CDS access controls (DCL)', collection: `${A}/acm/dcl/sources`, since: { basis: 740, sp: 10, label: 'ABAP 7.40 SP10' }, provenOnOurBox: true },
|
|
8
|
+
'rap.bdef': { label: 'Behavior definitions (RAP)', collection: `${A}/bo/behaviordefinitions`, since: { basis: 753, label: 'S/4HANA 1809 (ABAP 7.53)' }, provenOnOurBox: true },
|
|
9
|
+
// Final review I1 (owner rule: a wrong no is worse than unknown): the next six
|
|
10
|
+
// rows had release floors with no cited source (gap table §3.1 gives one date
|
|
11
|
+
// for five RAP/CDS objects; "1709" for table/structure; "1610 / 7.50" for
|
|
12
|
+
// domain/data element). Dropped: listed = yes, else unknown, never no. Add a
|
|
13
|
+
// floor back only with a citation (SAP Help release line or ADT feature matrix).
|
|
14
|
+
'rap.srvd': { label: 'Service definitions', collection: `${A}/ddic/srvd/sources`, provenOnOurBox: true },
|
|
15
|
+
'rap.srvb': { label: 'Service bindings', collection: `${A}/businessservices/bindings`, provenOnOurBox: true },
|
|
16
|
+
'ddic.tables': { label: 'Tables in ADT', collection: `${A}/ddic/tables`, provenOnOurBox: true },
|
|
17
|
+
'ddic.structures': { label: 'Structures in ADT', collection: `${A}/ddic/structures`, provenOnOurBox: true },
|
|
18
|
+
'ddic.domains': { label: 'Domains in ADT', collection: `${A}/ddic/domains`, provenOnOurBox: true },
|
|
19
|
+
'ddic.dataelements': { label: 'Data elements in ADT', collection: `${A}/ddic/dataelements`, provenOnOurBox: true },
|
|
20
|
+
'generator.repository': { label: "SAP's RAP generator", collection: `${A}/repository/generators`, since: { basis: 757, label: 'S/4HANA 2022 (ABAP 7.57)' }, absentMeansNo: true, provenOnOurBox: true },
|
|
21
|
+
'atc.worklists': { label: 'ATC checks', collection: `${A}/atc/worklists`, since: { basis: 740, label: 'ABAP 7.40' }, provenOnOurBox: true },
|
|
22
|
+
'unit.testruns': { label: 'ABAP Unit runs', collection: `${A}/abapunit/testruns`, provenOnOurBox: true },
|
|
23
|
+
'text.programs': { label: 'Text elements of programs', collection: `${A}/textelements/programs`, since: { basis: 740, sp: 8, label: 'ABAP 7.40 SP08' }, provenOnOurBox: true },
|
|
24
|
+
// No `since` (R4): the profile's own CVERS reads use this endpoint, so a
|
|
25
|
+
// release-based `no` could only ever be wrong. Listed = yes, else unknown.
|
|
26
|
+
'datapreview.freestyle': { label: 'SQL data preview', collection: `${A}/datapreview/freestyle`, provenOnOurBox: true },
|
|
27
|
+
'msag': { label: 'Message classes', collection: `${A}/messageclass`, provenOnOurBox: true },
|
|
28
|
+
'nrob': { label: 'Number range objects', collection: `${A}/numberranges/objects`, provenOnOurBox: true },
|
|
29
|
+
});
|
|
30
|
+
export const FEATURE_IDS = Object.freeze(Object.keys(FEATURES));
|
|
31
|
+
export function isFeatureId(s) {
|
|
32
|
+
return Object.prototype.hasOwnProperty.call(FEATURES, s);
|
|
33
|
+
}
|
|
34
|
+
/** url-mapper type token (upper case) → the feature it needs. Unlisted types need nothing. */
|
|
35
|
+
export const OBJECT_TYPE_FEATURE = Object.freeze({
|
|
36
|
+
BDEF: 'rap.bdef', SRVD: 'rap.srvd', SRVB: 'rap.srvb',
|
|
37
|
+
DDLS: 'cds.ddls', CDS: 'cds.ddls', DDLX: 'cds.ddlx', DCLS: 'cds.dcls',
|
|
38
|
+
TABL: 'ddic.tables', STRU: 'ddic.structures', DOMA: 'ddic.domains', DTEL: 'ddic.dataelements',
|
|
39
|
+
MSAG: 'msag', NROB: 'nrob',
|
|
40
|
+
});
|
|
41
|
+
/** The RAP/CDS subset: the only types a READ is ever refused for (R6). */
|
|
42
|
+
export const READ_GATED_TYPES = new Set(['BDEF', 'SRVD', 'SRVB', 'DDLS', 'CDS', 'DDLX', 'DCLS']);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { FEATURES, OBJECT_TYPE_FEATURE, READ_GATED_TYPES } from './profile-features.js';
|
|
2
|
+
import { supports, systemLabel } from './system-profile.js';
|
|
3
|
+
export const NOT_ON_THIS_RELEASE = 'not_on_this_release';
|
|
4
|
+
/** For tools whose `type` argument names an object type (url-mapper token). */
|
|
5
|
+
export function byObjectType(args) {
|
|
6
|
+
const t = typeof args?.type === 'string' ? args.type.trim().toUpperCase() : '';
|
|
7
|
+
return t && Object.prototype.hasOwnProperty.call(OBJECT_TYPE_FEATURE, t) ? OBJECT_TYPE_FEATURE[t] : null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* R6 — reads refuse only RAP/CDS types (they cannot exist below their release).
|
|
11
|
+
* DDIC types carry no release floor at all (final review I1); a DDIC read was never proven to fail, and a
|
|
12
|
+
* wrong read costs one 404 while a wrong `no` costs the user's work.
|
|
13
|
+
*/
|
|
14
|
+
export function byObjectTypeForRead(args) {
|
|
15
|
+
const t = typeof args?.type === 'string' ? args.type.trim().toUpperCase() : '';
|
|
16
|
+
return READ_GATED_TYPES.has(t) ? byObjectType(args) : null;
|
|
17
|
+
}
|
|
18
|
+
export function requiredFeature(tool, args) {
|
|
19
|
+
const r = tool.requires;
|
|
20
|
+
if (!r)
|
|
21
|
+
return null;
|
|
22
|
+
return typeof r === 'function' ? r(args) : r;
|
|
23
|
+
}
|
|
24
|
+
export function profileGate(tool, args, profile) {
|
|
25
|
+
if (!profile)
|
|
26
|
+
return null;
|
|
27
|
+
const feature = requiredFeature(tool, args);
|
|
28
|
+
if (!feature)
|
|
29
|
+
return null;
|
|
30
|
+
const v = supports(profile, feature);
|
|
31
|
+
if (v.answer !== 'no')
|
|
32
|
+
return null;
|
|
33
|
+
// I2 — name when the facts were read: the profile is kept up to 30 days.
|
|
34
|
+
return { feature, message: `${FEATURES[feature].label}: ${v.reason}. This system: ${systemLabel(profile)}, read ${profile.builtAt.slice(0, 10)}.` };
|
|
35
|
+
}
|
|
36
|
+
/** Names of the tools whose FIXED feature is a definite no. [] for no profile. */
|
|
37
|
+
export function hiddenByProfile(tools, profile) {
|
|
38
|
+
if (!profile)
|
|
39
|
+
return [];
|
|
40
|
+
return tools.filter((t) => typeof t.requires === 'string' && supports(profile, t.requires).answer === 'no').map((t) => t.name);
|
|
41
|
+
}
|
|
42
|
+
/** Drop the named tools. Same array back when nothing changes (byte-identical path). */
|
|
43
|
+
export function withoutTools(tools, hidden) {
|
|
44
|
+
if (hidden.length === 0)
|
|
45
|
+
return tools;
|
|
46
|
+
const set = new Set(hidden);
|
|
47
|
+
const kept = tools.filter((t) => !set.has(t.name));
|
|
48
|
+
return kept.length === tools.length ? tools : kept;
|
|
49
|
+
}
|
|
50
|
+
export function toolsForProfile(tools, profile) {
|
|
51
|
+
return withoutTools(tools, hiddenByProfile(tools, profile));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* R7 / D4 — tools[] must be byte-stable for the session (prompt cache,
|
|
55
|
+
* preserved thinking), but it is rebuilt every turn. The hidden set is pinned
|
|
56
|
+
* on the session the first time an alias is seen — whatever the profile was
|
|
57
|
+
* on that turn (null pins []) — and reused after that. Another alias re-pins.
|
|
58
|
+
* A profile that arrives later refuses per call at dispatch instead.
|
|
59
|
+
*/
|
|
60
|
+
export function pinHiddenTools(session, alias, tools, profile) {
|
|
61
|
+
const pinned = session.profile_hidden_tools;
|
|
62
|
+
if (pinned && pinned.alias === alias)
|
|
63
|
+
return pinned.names;
|
|
64
|
+
const names = hiddenByProfile(tools, profile);
|
|
65
|
+
session.profile_hidden_tools = { alias, names };
|
|
66
|
+
return names;
|
|
67
|
+
}
|
package/dist/sap/system-info.js
CHANGED
|
@@ -535,6 +535,6 @@ export function renderSapSystemBlock(info) {
|
|
|
535
535
|
].join(' ');
|
|
536
536
|
return `<sap_system ${attrs} />`;
|
|
537
537
|
}
|
|
538
|
-
function escapeAttr(s) {
|
|
538
|
+
export function escapeAttr(s) {
|
|
539
539
|
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
540
540
|
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// cspeach-cli/src/sap/system-profile.ts
|
|
2
|
+
/**
|
|
3
|
+
* System profile (ADT gap program, Part 1; spec §2.1).
|
|
4
|
+
*
|
|
5
|
+
* One profile per connected SAP system, built from the facts the CLI already
|
|
6
|
+
* has (system-info: CVERS release) plus two reads cached for 30 days: the ADT
|
|
7
|
+
* discovery document and the SAP_BASIS support pack. `supports()` answers
|
|
8
|
+
* yes / no / unknown with a plain reason. Unknown changes nothing: tools run
|
|
9
|
+
* as today and fail at HTTP time if the feature is missing. Only a definite
|
|
10
|
+
* `no` stops a call before it reaches SAP.
|
|
11
|
+
*
|
|
12
|
+
* Evidence rules (owner decision D1): a collection LISTED by discovery is a
|
|
13
|
+
* yes; an unlisted one proves nothing (our S/4HANA 2023 box omits endpoints
|
|
14
|
+
* that work). A `no` comes from the release table alone, except the generator,
|
|
15
|
+
* which keeps today's getGeneratorFact reading (absent = no = CSPeach writer).
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { auditLog } from '@cspeach/sap-client';
|
|
20
|
+
import { cspeachRoot } from '../config/paths.js';
|
|
21
|
+
import { escapeAttr } from './system-info.js';
|
|
22
|
+
import { FEATURES, FEATURE_IDS } from './profile-features.js';
|
|
23
|
+
export function releaseAtLeast(p, min) {
|
|
24
|
+
if (p.basis === null)
|
|
25
|
+
return null;
|
|
26
|
+
if (p.basis > min.basis)
|
|
27
|
+
return true;
|
|
28
|
+
if (p.basis < min.basis)
|
|
29
|
+
return false;
|
|
30
|
+
if (min.sp === undefined)
|
|
31
|
+
return true;
|
|
32
|
+
if (p.sp === null)
|
|
33
|
+
return null;
|
|
34
|
+
return p.sp >= min.sp;
|
|
35
|
+
}
|
|
36
|
+
export function systemLabel(p) {
|
|
37
|
+
if (p.basis === null && p.release === 'unknown')
|
|
38
|
+
return 'unknown release';
|
|
39
|
+
const sp = p.sp === null ? '' : ` SP${String(p.sp).padStart(2, '0')}`;
|
|
40
|
+
const abap = p.abapVersion === 'unknown' ? '' : ` (ABAP ${p.abapVersion})`;
|
|
41
|
+
return `${p.release}${sp}${abap}`;
|
|
42
|
+
}
|
|
43
|
+
export function supports(p, id) {
|
|
44
|
+
const rule = FEATURES[id];
|
|
45
|
+
if (!p)
|
|
46
|
+
return { answer: 'unknown', reason: 'system profile not available' };
|
|
47
|
+
const listed = !!rule.collection && p.discovery.read && p.discovery.collections.includes(rule.collection);
|
|
48
|
+
if (listed)
|
|
49
|
+
return { answer: 'yes', reason: 'listed by this system' };
|
|
50
|
+
const byRelease = rule.since ? releaseAtLeast(p, rule.since) : null;
|
|
51
|
+
if (rule.since && byRelease === false)
|
|
52
|
+
return { answer: 'no', reason: `needs ${rule.since.label} or later` };
|
|
53
|
+
// R3 — the generator must agree with getGeneratorFact for the same body:
|
|
54
|
+
// list unread = fact unknown (HTTP ≥ 400) or no (HTML page), never a yes by release.
|
|
55
|
+
if (rule.absentMeansNo && rule.collection && !p.discovery.read)
|
|
56
|
+
return { answer: 'unknown', reason: 'the system list could not be read' };
|
|
57
|
+
if (rule.absentMeansNo && rule.collection && p.discovery.read)
|
|
58
|
+
return { answer: 'no', reason: 'not offered by this system' };
|
|
59
|
+
if (rule.since && byRelease === true)
|
|
60
|
+
return { answer: 'yes', reason: `${systemLabel(p)} has it` };
|
|
61
|
+
if (rule.since && p.basis !== null && p.basis === rule.since.basis && p.sp === null) {
|
|
62
|
+
return { answer: 'unknown', reason: `needs ${rule.since.label}; this system is ${p.abapVersion} with an unknown support pack` };
|
|
63
|
+
}
|
|
64
|
+
if (!p.discovery.read && rule.collection)
|
|
65
|
+
return { answer: 'unknown', reason: 'the system list could not be read' };
|
|
66
|
+
return { answer: 'unknown', reason: 'no release fact covers it' };
|
|
67
|
+
}
|
|
68
|
+
function basisOf(info) {
|
|
69
|
+
const raw = info?.components?.['SAP_BASIS'];
|
|
70
|
+
if (!raw || !/^\d{3}$/.test(raw.trim()))
|
|
71
|
+
return null;
|
|
72
|
+
return Number(raw.trim());
|
|
73
|
+
}
|
|
74
|
+
export function profileFrom(i) {
|
|
75
|
+
if (!i.info && !i.discovery)
|
|
76
|
+
return null;
|
|
77
|
+
const basis = basisOf(i.info);
|
|
78
|
+
return {
|
|
79
|
+
alias: i.alias,
|
|
80
|
+
baseUrl: i.baseUrl,
|
|
81
|
+
platform: i.info?.platform ?? 'unknown',
|
|
82
|
+
release: i.info?.release ?? 'unknown',
|
|
83
|
+
abapVersion: i.info?.abapVersion ?? 'unknown',
|
|
84
|
+
basis,
|
|
85
|
+
sp: basis === null ? null : i.sp,
|
|
86
|
+
cloud: i.info && Object.keys(i.info.components).length > 0 ? 'no' : 'unknown',
|
|
87
|
+
discovery: i.discovery
|
|
88
|
+
? { read: true, collections: Object.keys(i.discovery.collections).sort() }
|
|
89
|
+
: { read: false, collections: [] },
|
|
90
|
+
builtAt: i.now.toISOString(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
94
|
+
/** Bump when the stored shape or its meaning changes. */
|
|
95
|
+
const CACHE_VERSION = 1;
|
|
96
|
+
const SP_SQL = "SELECT COMPONENT, RELEASE, EXTRELEASE FROM CVERS WHERE COMPONENT = 'SAP_BASIS'";
|
|
97
|
+
/**
|
|
98
|
+
* Final review I4 — an incomplete build (list or SP not read) is never written
|
|
99
|
+
* as a profile (R2), so without this it would cost both reads on every CLI
|
|
100
|
+
* start. The ATTEMPT is remembered per alias for a day (when, and against
|
|
101
|
+
* which system — no answers), and each read is bounded in time.
|
|
102
|
+
*/
|
|
103
|
+
const ATTEMPT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
104
|
+
const READ_TIMEOUT_MS = 15_000;
|
|
105
|
+
/** Last build per alias for this process — null included (X3: a failed turn is not retried every turn). */
|
|
106
|
+
const memCache = new Map();
|
|
107
|
+
export function _resetSystemProfileCache() {
|
|
108
|
+
memCache.clear();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The SAP_BASIS support pack from the column-major data preview ('0002' → 2).
|
|
112
|
+
* Sibling of parseCvers (system-info.ts), which reads the same shape into
|
|
113
|
+
* component → release; that function and its SQL stay untouched (D2).
|
|
114
|
+
*/
|
|
115
|
+
export function parseExtRelease(xml) {
|
|
116
|
+
const colsRe = /<(?:[\w:]+:)?columns\b[^>]*>([\s\S]*?)<\/(?:[\w:]+:)?columns\s*>/g;
|
|
117
|
+
let m;
|
|
118
|
+
while ((m = colsRe.exec(xml)) !== null) {
|
|
119
|
+
const block = m[1];
|
|
120
|
+
const name = /<(?:[\w:]+:)?metadata\b[^>]*?\b(?:[\w:]+:)?name="([^"]+)"/i.exec(block)?.[1];
|
|
121
|
+
if (!name || name.toUpperCase() !== 'EXTRELEASE')
|
|
122
|
+
continue;
|
|
123
|
+
const value = /<(?:[\w:]+:)?data\b(?:\s[^>]*?)?>([\s\S]*?)<\/(?:[\w:]+:)?data\s*>/.exec(block)?.[1]?.trim() ?? '';
|
|
124
|
+
const digits = /(\d+)\s*$/.exec(value)?.[1];
|
|
125
|
+
return digits ? Number(digits) : null;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
export async function getSystemProfile(adt, alias, info, opts = {}) {
|
|
130
|
+
const env = opts.env ?? process.env;
|
|
131
|
+
if ((env['CSPEACH_SYSTEM_PROFILE'] ?? '').trim().toLowerCase() === 'off')
|
|
132
|
+
return null;
|
|
133
|
+
const now = opts.now ?? (() => new Date());
|
|
134
|
+
const conn = adt.connection;
|
|
135
|
+
const baseUrl = conn?.getBaseUrl?.() ?? '';
|
|
136
|
+
if (!opts.fresh) {
|
|
137
|
+
if (memCache.has(alias)) {
|
|
138
|
+
const hit = memCache.get(alias) ?? null;
|
|
139
|
+
if (hit === null || sameSystem(hit, baseUrl, info))
|
|
140
|
+
return hit;
|
|
141
|
+
}
|
|
142
|
+
const fromDisk = readDiskCache(alias, now());
|
|
143
|
+
if (fromDisk && sameSystem(fromDisk, baseUrl, info)) {
|
|
144
|
+
memCache.set(alias, fromDisk);
|
|
145
|
+
return fromDisk;
|
|
146
|
+
}
|
|
147
|
+
// I4 — an incomplete build was tried for this system within the day: no
|
|
148
|
+
// profile this start (unknown everywhere = today's behaviour), no requests.
|
|
149
|
+
if (recentAttempt(alias, baseUrl, info, now())) {
|
|
150
|
+
memCache.set(alias, null);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const timeoutMs = opts.readTimeoutMs ?? READ_TIMEOUT_MS;
|
|
155
|
+
const startedAt = Date.now();
|
|
156
|
+
try {
|
|
157
|
+
let discovery = null;
|
|
158
|
+
try {
|
|
159
|
+
discovery = await bounded(adt.discovery(), timeoutMs);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
discovery = null;
|
|
163
|
+
}
|
|
164
|
+
let sp = null;
|
|
165
|
+
if (info?.components?.['SAP_BASIS'] && conn?.post) {
|
|
166
|
+
try {
|
|
167
|
+
const resp = await bounded(conn.post('/sap/bc/adt/datapreview/freestyle?rowNumber=5', SP_SQL, 'text/plain', 'application/vnd.sap.adt.datapreview.table.v1+xml'), timeoutMs);
|
|
168
|
+
if (resp && resp.status === 200)
|
|
169
|
+
sp = parseExtRelease(resp.body);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
sp = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const profile = profileFrom({ alias, baseUrl, info, sp, discovery, now: now() });
|
|
176
|
+
if (!profile) {
|
|
177
|
+
memCache.set(alias, null);
|
|
178
|
+
writeAttempt(alias, baseUrl, info, now());
|
|
179
|
+
await auditLog('sapProfile', 'ALIAS', alias, 'error', Date.now() - startedAt, 'no discovery and no system info');
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
memCache.set(alias, profile);
|
|
183
|
+
// R2 — same rule as system-info's failed probe: never persist what was not
|
|
184
|
+
// read. Complete = list read, and the SP read (or no SAP_BASIS to read it for).
|
|
185
|
+
const complete = profile.discovery.read && (profile.sp !== null || profile.basis === null);
|
|
186
|
+
if (complete) {
|
|
187
|
+
writeDiskCache(alias, profile);
|
|
188
|
+
clearAttempt(alias);
|
|
189
|
+
}
|
|
190
|
+
else
|
|
191
|
+
writeAttempt(alias, baseUrl, info, now());
|
|
192
|
+
await auditLog('sapProfile', 'ALIAS', alias, 'success', Date.now() - startedAt, `release=${profile.release} sp=${profile.sp ?? 'unknown'} discovery=${profile.discovery.read ? profile.discovery.collections.length : 'not read'} cached=${complete ? 'disk' : 'memory'}`);
|
|
193
|
+
return profile;
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
memCache.set(alias, null);
|
|
197
|
+
writeAttempt(alias, baseUrl, info, now());
|
|
198
|
+
await auditLog('sapProfile', 'ALIAS', alias, 'error', Date.now() - startedAt, `threw: ${err.message}`);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function sameSystem(p, baseUrl, info) {
|
|
203
|
+
if (baseUrl && p.baseUrl && p.baseUrl !== baseUrl)
|
|
204
|
+
return false;
|
|
205
|
+
if (!info)
|
|
206
|
+
return true;
|
|
207
|
+
return p.release === info.release && p.abapVersion === info.abapVersion;
|
|
208
|
+
}
|
|
209
|
+
function cacheFile(alias, kind = 'profile') {
|
|
210
|
+
const safe = alias.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
211
|
+
return join(cspeachRoot(), 'sap-cache', `${safe}.${kind}.json`);
|
|
212
|
+
}
|
|
213
|
+
/** Resolves to null when the read takes longer than `ms` (the request itself is left to finish). */
|
|
214
|
+
function bounded(p, ms) {
|
|
215
|
+
let timer;
|
|
216
|
+
const timeout = new Promise((resolve) => {
|
|
217
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
218
|
+
timer.unref?.();
|
|
219
|
+
});
|
|
220
|
+
return Promise.race([p, timeout]).finally(() => { if (timer)
|
|
221
|
+
clearTimeout(timer); });
|
|
222
|
+
}
|
|
223
|
+
function recentAttempt(alias, baseUrl, info, now) {
|
|
224
|
+
try {
|
|
225
|
+
const path = cacheFile(alias, 'profile-attempt');
|
|
226
|
+
if (!existsSync(path))
|
|
227
|
+
return false;
|
|
228
|
+
const a = JSON.parse(readFileSync(path, 'utf8'));
|
|
229
|
+
const at = new Date(a.attemptedAt).getTime();
|
|
230
|
+
if (!Number.isFinite(at) || now.getTime() - at > ATTEMPT_TTL_MS || now.getTime() < at)
|
|
231
|
+
return false;
|
|
232
|
+
if (a.baseUrl !== baseUrl)
|
|
233
|
+
return false;
|
|
234
|
+
return a.release === (info?.release ?? 'unknown') && a.abapVersion === (info?.abapVersion ?? 'unknown');
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function writeAttempt(alias, baseUrl, info, now) {
|
|
241
|
+
try {
|
|
242
|
+
const dir = join(cspeachRoot(), 'sap-cache');
|
|
243
|
+
if (!existsSync(dir))
|
|
244
|
+
mkdirSync(dir, { recursive: true });
|
|
245
|
+
const a = { attemptedAt: now.toISOString(), baseUrl, release: info?.release ?? 'unknown', abapVersion: info?.abapVersion ?? 'unknown' };
|
|
246
|
+
writeFileSync(cacheFile(alias, 'profile-attempt'), JSON.stringify(a, null, 2), 'utf8');
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// best effort — at worst the next start tries the build again
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function clearAttempt(alias) {
|
|
253
|
+
try {
|
|
254
|
+
rmSync(cacheFile(alias, 'profile-attempt'), { force: true });
|
|
255
|
+
}
|
|
256
|
+
catch { /* best effort */ }
|
|
257
|
+
}
|
|
258
|
+
function readDiskCache(alias, now) {
|
|
259
|
+
try {
|
|
260
|
+
const path = cacheFile(alias);
|
|
261
|
+
if (!existsSync(path))
|
|
262
|
+
return null;
|
|
263
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
264
|
+
if (raw.cacheVersion !== CACHE_VERSION || !raw.builtAt)
|
|
265
|
+
return null;
|
|
266
|
+
if (now.getTime() - new Date(raw.builtAt).getTime() > CACHE_TTL_MS)
|
|
267
|
+
return null;
|
|
268
|
+
const { cacheVersion: _v, ...profile } = raw;
|
|
269
|
+
return profile;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function writeDiskCache(alias, profile) {
|
|
276
|
+
try {
|
|
277
|
+
const dir = join(cspeachRoot(), 'sap-cache');
|
|
278
|
+
if (!existsSync(dir))
|
|
279
|
+
mkdirSync(dir, { recursive: true });
|
|
280
|
+
writeFileSync(cacheFile(alias), JSON.stringify({ ...profile, cacheVersion: CACHE_VERSION }, null, 2), 'utf8');
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// best effort — the memory result is still returned
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** The one-line summary for <session_context>; '' when there is no profile. */
|
|
287
|
+
export function renderSapProfileBlock(p) {
|
|
288
|
+
if (!p)
|
|
289
|
+
return '';
|
|
290
|
+
const answer = (id) => supports(p, id).answer;
|
|
291
|
+
const viewEntity = answer('cds.viewEntity');
|
|
292
|
+
const cdsSyntax = viewEntity === 'yes' ? 'define view entity' : viewEntity === 'no' ? 'define view' : 'unknown';
|
|
293
|
+
const notAvailable = FEATURE_IDS
|
|
294
|
+
.map((id) => ({ id, v: supports(p, id) }))
|
|
295
|
+
.filter((x) => x.v.answer === 'no')
|
|
296
|
+
.map((x) => `${FEATURES[x.id].label}: ${x.v.reason}`)
|
|
297
|
+
.join('; ') || 'none known';
|
|
298
|
+
const attrs = [
|
|
299
|
+
`release="${escapeAttr(p.release)}"`,
|
|
300
|
+
`sp="${p.sp === null ? 'unknown' : `SP${String(p.sp).padStart(2, '0')}`}"`,
|
|
301
|
+
`abap="${escapeAttr(p.abapVersion)}"`,
|
|
302
|
+
`cloud="${p.cloud}"`,
|
|
303
|
+
`cds_view_syntax="${cdsSyntax}"`,
|
|
304
|
+
`rap="${answer('rap.bdef')}"`,
|
|
305
|
+
`rap_generator="${answer('generator.repository')}"`,
|
|
306
|
+
`table_entity="${answer('cds.tableEntity')}"`,
|
|
307
|
+
`discovery="${p.discovery.read ? 'read' : 'not read'}"`,
|
|
308
|
+
`built="${p.builtAt.slice(0, 10)}"`,
|
|
309
|
+
`not_available="${escapeAttr(notAvailable)}"`,
|
|
310
|
+
];
|
|
311
|
+
return `<sap_profile ${attrs.join(' ')} />`;
|
|
312
|
+
}
|
|
@@ -96,6 +96,7 @@ export async function rapFinishCheckHandler(args, ctx) {
|
|
|
96
96
|
}
|
|
97
97
|
registerTool({
|
|
98
98
|
name: 'rap_finish_check',
|
|
99
|
+
requires: 'rap.bdef',
|
|
99
100
|
description: 'Check the Finish items F1–F5 of a RAP app against its live sources (authorization, access control, '
|
|
100
101
|
+ 'read-only admin fields, labels, coded fields). Read-only. Run it after the Finish step and before the UI. '
|
|
101
102
|
+ 'A failed item lists what to fix; fix only those and run it again.',
|
|
@@ -294,6 +294,7 @@ export async function sapGenerateObjectsHandler(args, ctx, deps = {}) {
|
|
|
294
294
|
}
|
|
295
295
|
registerTool({
|
|
296
296
|
name: 'sap_generate_objects',
|
|
297
|
+
requires: 'generator.repository',
|
|
297
298
|
description: "Run SAP's RAP generator for a skeleton phase whose Plan block says sap-generator. Pass the decision path and "
|
|
298
299
|
+ 'sha256 from the plan block, and one approval_id per planned object in the same order (request_approval with '
|
|
299
300
|
+ 'op create for each). CSPeach validates, previews, generates, then checks every object is active and on the '
|
package/dist/tools/sap-read.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* badiList(query, enhancementSpot?, maxResults?)
|
|
23
23
|
*/
|
|
24
24
|
import { registerTool } from './index.js';
|
|
25
|
+
import { byObjectTypeForRead } from '../sap/profile-gate.js';
|
|
25
26
|
import { normaliseSyntaxResult, checkSyntax } from './verify.js';
|
|
26
27
|
import { recordSyntaxCheck } from './syntax-state.js';
|
|
27
28
|
import { spillToolResult } from './result-spill.js';
|
|
@@ -71,6 +72,7 @@ export function formatSearchHits(hits) {
|
|
|
71
72
|
// -----------------------------------------------------------------------
|
|
72
73
|
registerTool({
|
|
73
74
|
name: 'sap_get_source',
|
|
75
|
+
requires: byObjectTypeForRead,
|
|
74
76
|
description: 'Read the source code of an ABAP object by name and type. '
|
|
75
77
|
+ 'Optional `version` selects active or inactive source explicitly; '
|
|
76
78
|
+ 'omitting it lets SAP choose (usually active). When the object has '
|
|
@@ -174,6 +176,7 @@ registerTool({
|
|
|
174
176
|
// -----------------------------------------------------------------------
|
|
175
177
|
registerTool({
|
|
176
178
|
name: 'sap_object_structure',
|
|
179
|
+
requires: byObjectTypeForRead,
|
|
177
180
|
description: 'Retrieve metadata and structural information for an ABAP repository object (package, description, lock state, etc.).',
|
|
178
181
|
isMutating: false,
|
|
179
182
|
requiresSap: true,
|
|
@@ -281,6 +284,7 @@ export function arsColumnErrorHint(query, err) {
|
|
|
281
284
|
}
|
|
282
285
|
registerTool({
|
|
283
286
|
name: 'sap_sql_query',
|
|
287
|
+
requires: 'datapreview.freestyle',
|
|
284
288
|
description: 'Execute a read-only SQL SELECT against the SAP database via ADT Data Preview. Security-sensitive tables are blocked automatically. '
|
|
285
289
|
+ 'Dialect limits (violations return an opaque "SAP HTTP 400: sqlQuery"): no IN-lists and at most one LIKE per WHERE clause '
|
|
286
290
|
+ '(multi-OR LIKE chains are rejected) — use a single LIKE or split into separate queries. '
|
|
@@ -376,6 +380,7 @@ registerTool({
|
|
|
376
380
|
// -----------------------------------------------------------------------
|
|
377
381
|
registerTool({
|
|
378
382
|
name: 'sap_atc_run',
|
|
383
|
+
requires: 'atc.worklists',
|
|
379
384
|
description: 'Run ABAP Test Cockpit (ATC) checks on a specific object and return all findings.',
|
|
380
385
|
isMutating: false,
|
|
381
386
|
requiresSap: true,
|
|
@@ -394,8 +399,29 @@ registerTool({
|
|
|
394
399
|
handler: async (args, ctx) => {
|
|
395
400
|
// atcRun(type, name, variant?) — type first
|
|
396
401
|
// Review fix round 1, M-3: Esc / Ctrl+C stops a long ATC run.
|
|
397
|
-
const
|
|
398
|
-
|
|
402
|
+
const variant = args.variant ?? 'DEFAULT';
|
|
403
|
+
const result = await ctx.adt.atcRun(args.type, args.name, variant, ctx.signal ? { signal: ctx.signal } : {});
|
|
404
|
+
// Fable review 2026-10-15, I-1: a finished run whose worklist never named
|
|
405
|
+
// the object did not check it (the variant has no check for that type).
|
|
406
|
+
// Say so plainly; "0 findings" there is not a measurement. Every other
|
|
407
|
+
// result is the same JSON as before (the coverage key is not added).
|
|
408
|
+
const { coverage, ...shown } = result;
|
|
409
|
+
if (coverage === 'absent' && result.totalFindings === 0) {
|
|
410
|
+
const objectName = `${String(args.type).toUpperCase()} ${String(args.name).toUpperCase()}`;
|
|
411
|
+
return {
|
|
412
|
+
content: [
|
|
413
|
+
`ATC did not check ${objectName}.`,
|
|
414
|
+
'SAP finished the run, but its worklist never listed this object.',
|
|
415
|
+
'This is not a measurement. Do not report it as 0 findings or as clean.',
|
|
416
|
+
'Likely causes:',
|
|
417
|
+
`- the ${String(variant).toUpperCase()} check variant may have no check for this object type (seen on S4H for DDLS, DDLX, SRVD, DOMA);`,
|
|
418
|
+
'- the name may be wrong, or the object may not exist or not be active.',
|
|
419
|
+
'Use sap_search_object to confirm the object exists.',
|
|
420
|
+
`(ATC worklist ${result.worklistId || 'unknown'})`,
|
|
421
|
+
].join('\n'),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return { content: JSON.stringify(shown, null, 2) };
|
|
399
425
|
},
|
|
400
426
|
});
|
|
401
427
|
// -----------------------------------------------------------------------
|
|
@@ -631,6 +657,7 @@ registerTool({
|
|
|
631
657
|
// -----------------------------------------------------------------------
|
|
632
658
|
registerTool({
|
|
633
659
|
name: 'sap_run_unit_test',
|
|
660
|
+
requires: 'unit.testruns',
|
|
634
661
|
description: 'Execute ABAP Unit tests for an object and return a structured pass/fail report with method-level results.',
|
|
635
662
|
isMutating: false,
|
|
636
663
|
requiresSap: true,
|
package/dist/tools/sap-write.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import crypto from 'node:crypto';
|
|
25
25
|
import { readFileSync } from 'node:fs';
|
|
26
26
|
import { registerTool } from './index.js';
|
|
27
|
+
import { byObjectType } from '../sap/profile-gate.js';
|
|
27
28
|
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
28
29
|
import { grantClaimMismatch, grantActivateMismatch, grantOwningTransportMismatch, grantOwningTransportDetail } from '../approvals/grant-claims.js';
|
|
29
30
|
import { autoSnapshot, autoSnapshotClassInclude, autoSnapshotMessageClass } from './snapshot.js';
|
|
@@ -172,6 +173,7 @@ function snapshotSource(snap) {
|
|
|
172
173
|
}
|
|
173
174
|
registerTool({
|
|
174
175
|
name: 'sap_set_source',
|
|
176
|
+
requires: byObjectType,
|
|
175
177
|
description: 'Overwrite the full source of an ABAP object. Requires approval_id. '
|
|
176
178
|
+ 'Use for class-definition changes, new methods, or any change not confined to '
|
|
177
179
|
+ 'a single method body. For method-body-only edits use sap_update_method (Rule 7a). '
|
|
@@ -567,6 +569,7 @@ registerTool({
|
|
|
567
569
|
// autoSnapshot here for belt-and-suspenders consistency with the CLI gate model.
|
|
568
570
|
registerTool({
|
|
569
571
|
name: 'sap_delete_object',
|
|
572
|
+
requires: byObjectType,
|
|
570
573
|
description: 'Delete an ABAP object. Requires approval_id. '
|
|
571
574
|
+ 'A snapshot is taken automatically before deletion. '
|
|
572
575
|
+ 'Post-check: confirms the object no longer exists. '
|
|
@@ -692,6 +695,7 @@ registerTool({
|
|
|
692
695
|
// No snapshot needed: new object has no prior source.
|
|
693
696
|
registerTool({
|
|
694
697
|
name: 'sap_create_object',
|
|
698
|
+
requires: byObjectType,
|
|
695
699
|
description: 'Create a new ABAP object shell. '
|
|
696
700
|
+ 'Requires approval_id. No snapshot needed (new object). '
|
|
697
701
|
+ 'Write source separately via sap_set_source, then call sap_activate. '
|
|
@@ -1125,6 +1129,7 @@ registerTool({
|
|
|
1125
1129
|
// No snapshot needed: new object has no prior source.
|
|
1126
1130
|
registerTool({
|
|
1127
1131
|
name: 'sap_create_domain',
|
|
1132
|
+
requires: 'ddic.domains',
|
|
1128
1133
|
description: 'Create a DDIC domain (DOMA), then activate and verify it. '
|
|
1129
1134
|
+ 'Requires approval_id. No snapshot needed (new object). '
|
|
1130
1135
|
+ 'Use the domain in a data element via sap_create_data_element.',
|
|
@@ -1266,6 +1271,7 @@ registerTool({
|
|
|
1266
1271
|
// No snapshot needed: new object has no prior source.
|
|
1267
1272
|
registerTool({
|
|
1268
1273
|
name: 'sap_create_data_element',
|
|
1274
|
+
requires: 'ddic.dataelements',
|
|
1269
1275
|
description: 'Create a DDIC data element (DTEL) bound to an existing domain, then '
|
|
1270
1276
|
+ 'activate and verify it. Requires approval_id. No snapshot needed (new object). '
|
|
1271
1277
|
+ 'Use the data element as a table/structure field type.',
|
|
@@ -1617,6 +1623,7 @@ registerTool({
|
|
|
1617
1623
|
// Signature: serviceBindingPublish(name) → Promise<{ status: number; error?: string }>
|
|
1618
1624
|
registerTool({
|
|
1619
1625
|
name: 'sap_service_binding_publish',
|
|
1626
|
+
requires: 'rap.srvb',
|
|
1620
1627
|
description: 'Publish an ADT service binding so its OData endpoint becomes active and reachable.',
|
|
1621
1628
|
isMutating: true,
|
|
1622
1629
|
requiresSap: true,
|
|
@@ -1640,6 +1647,7 @@ registerTool({
|
|
|
1640
1647
|
// NOTE: the unpublishjobs endpoint is inferred-not-yet-live-verified (VPN down).
|
|
1641
1648
|
registerTool({
|
|
1642
1649
|
name: 'sap_service_binding_unpublish',
|
|
1650
|
+
requires: 'rap.srvb',
|
|
1643
1651
|
description: 'Unpublish an ADT service binding so its OData endpoint is removed. '
|
|
1644
1652
|
+ 'Required before deleting a published binding (SAP returns HTTP 412 "is published" otherwise).',
|
|
1645
1653
|
isMutating: true,
|
|
@@ -1713,6 +1721,7 @@ function messageRowsProblem(rows) {
|
|
|
1713
1721
|
}
|
|
1714
1722
|
registerTool({
|
|
1715
1723
|
name: 'sap_message_maintain',
|
|
1724
|
+
requires: 'msag',
|
|
1716
1725
|
description: 'Add or change messages in an existing message class (MSAG, the T100 texts behind SE91). '
|
|
1717
1726
|
+ 'Create the class first with sap_create_object type MSAG. One call takes many messages. '
|
|
1718
1727
|
+ 'Each message: msgno (three digits, 000-999) and text (up to 73 characters, &1-&4 placeholders allowed). '
|
|
@@ -1915,6 +1924,7 @@ registerTool({
|
|
|
1915
1924
|
// in @cspeach/sap-client.
|
|
1916
1925
|
registerTool({
|
|
1917
1926
|
name: 'sap_number_range_intervals',
|
|
1927
|
+
requires: 'nrob',
|
|
1918
1928
|
description: 'STUB — not currently implemented; do not call. '
|
|
1919
1929
|
+ 'Writing NROB number-range intervals via ADT requires NR-object XML format '
|
|
1920
1930
|
+ 'not yet implemented in @cspeach/sap-client.',
|
|
@@ -103,6 +103,7 @@ async function screenNamesOf(ctx, name) {
|
|
|
103
103
|
// ── sap_get_text_elements ─────────────────────────────────────────────────────
|
|
104
104
|
registerTool({
|
|
105
105
|
name: 'sap_get_text_elements',
|
|
106
|
+
requires: 'text.programs',
|
|
106
107
|
description: 'Read a program\'s text elements: selection texts (labels of PARAMETERS / SELECT-OPTIONS), '
|
|
107
108
|
+ 'text symbols (TEXT-001) and list headings. Read-only. For selections it also lists '
|
|
108
109
|
+ 'selection-screen fields that still have NO text (without_text) — those show their raw name '
|
|
@@ -158,6 +159,7 @@ registerTool({
|
|
|
158
159
|
// ── sap_set_text_elements ─────────────────────────────────────────────────────
|
|
159
160
|
registerTool({
|
|
160
161
|
name: 'sap_set_text_elements',
|
|
162
|
+
requires: 'text.programs',
|
|
161
163
|
description: 'Add or change a program\'s text elements — one kind per call: selections (selection texts, max 30 chars, '
|
|
162
164
|
+ 'key = the PARAMETERS / SELECT-OPTIONS name), symbols (text symbols, key = the 3-char id of TEXT-xxx, max 132) '
|
|
163
165
|
+ 'or headings (listHeader max 70, columnHeader_1..4 max 132). MERGES with what is there: texts you do not name '
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cspeach/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.40",
|
|
4
4
|
"description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, signed skills, safety gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abap",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@anthropic-ai/sdk": "^0.90.0",
|
|
55
55
|
"@azure/msal-node": "6.0.1",
|
|
56
56
|
"@cspeach/register-core": "0.4.0",
|
|
57
|
-
"@cspeach/sap-client": "0.1.
|
|
57
|
+
"@cspeach/sap-client": "0.1.25",
|
|
58
58
|
"@iarna/toml": "^2.2.5",
|
|
59
59
|
"@inkjs/ui": "^2.0.0",
|
|
60
60
|
"@inquirer/prompts": "^7.0.0",
|