sap-ai-dev-toolkit 0.9.9920 → 0.9.9922

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/NOTICE CHANGED
@@ -2,7 +2,8 @@ Third-party notices for sap-ai-dev-toolkit
2
2
 
3
3
  This package includes patched VSP (vibing-steampunk) binaries built from upstream commit
4
4
  9886d2727f47506368b0a3c2f1c1766f1200f747. The BAS proxy-auth change is documented in
5
- patches/vsp-bas-proxy-auth.patch. The following upstream notice is reproduced from VSP:
5
+ patches/vsp-bas-proxy-auth.patch; the namespaced class-include EditSource fix is documented in
6
+ patches/vsp-namespaced-class-include-edit.patch. The following upstream notice is reproduced from VSP:
6
7
 
7
8
  vibing-steampunk (vsp)
8
9
  Copyright (c) 2025-2026 Alice Vinogradova and contributors
package/README.md CHANGED
@@ -234,7 +234,7 @@ The separate `integration-suite` stdio MCP server covers bounded discovery, auth
234
234
  Interactive `sap-ai-dev --setup` asks **"Add the SAP Integration Suite tools?"** before writing MCP configuration, preserves existing entries, and shows configured identities without credential values. Enter defaults to yes when credentials exist, no otherwise. `--tools` includes the companion checkbox; the empty-landscape menu offers Integration Suite-only setup. Run **`sap-ai-dev --setup-integration`** to configure only this server.
235
235
 
236
236
  - Detected credentials receive read-only capability checks. Core Cloud Foundry setup discovers `it-rt/api`, `apimanagement-apiportal/apiportal-apiaccess` and `apimanagement-devportal/devportal-apiaccess`; it can reuse/create the `sap-ai-dev-integration` key and, with explicit approval, dedicated `sap-ai-dev-integration-cpi`, `sap-ai-dev-integration-apim` or `sap-ai-dev-integration-devhub` instances. It pins plans/brokers visible to the targeted space, rejects ambiguous or other-organization plans, and does not grant entitlements.
237
- - Core service keys and separately typed optional records can be pasted as hidden JSON or supplied by downloaded file path. The user-only `~/.config/sap-ai-dev-toolkit/integration-suite.json` stores only normalized credentials (directory 0700/file 0600), never `mcp.json`; saving a key that cannot sign in requires confirmation.
237
+ - Core service keys and separately typed optional records can be pasted as hidden JSON or supplied by downloaded file path; the paste option first asks which service the key is for (Cloud Integration, API Management, Developer Hub or an optional record). The user-only `~/.config/sap-ai-dev-toolkit/integration-suite.json` stores only normalized credentials (directory 0700/file 0600), never `mcp.json`; saving a key that cannot sign in requires confirmation.
238
238
  - Optional discovery/setup accepts **existing credentials only**. It does not provision services, create optional service keys, activate capabilities, assign roles or retarget Cloud Foundry. Startup and capability probes never provision anything.
239
239
 
240
240
  Core precedence remains per-side: complete explicit `CPI_URL/CPI_TOKEN_URL/CPI_CLIENT_ID/CPI_CLIENT_SECRET` (or `APIM_*` / `DEVHUB_*`) → matching `*_SERVICE_KEY` JSON → one unambiguous `VCAP_SERVICES` binding → saved setup key. `*_VCAP_SERVICE` and `*_BINDING` select custom labels/instances. Certificate-based core keys are rejected. Optional environment JSON overrides only its matching stored record; malformed optional input disables that adapter, not usable core clients.
@@ -1,5 +1,5 @@
1
- 885ba61e5aa31e1bfe21953040a5cdb5e06c3eb648495f03b7cb035b2fd9d2c2 vsp-linux-x64
2
- 34f392bfbd853dd2b24abd95fda351d25633b538728371f881ee475a65904b31 vsp-linux-arm64
3
- cee27c146031fd932bf974ca359c6c0263c1ea1c20fe120f5244948af5ae1743 vsp-darwin-x64
4
- 320deb50652f0a29c8624c56e7cb150cb27a4e381b42c9b8c1edda1c3ad64a81 vsp-darwin-arm64
5
- 574885013902ccd93645ca7890cdff1c02ea769799df019f1d086d41f9e2cb71 vsp-win32-x64.exe
1
+ 31902921b0d2b5349be8e392aae3632ca8c3dee26950ea30c412ec83271722f3 vsp-linux-x64
2
+ df39b324fe419348f5157954d56c5f8d60e5bac28edc04a56e90c380b67de10d vsp-linux-arm64
3
+ 754338fbbbe66260cac722677753b16a0a2f23127b947bf1f3a189044fa66c32 vsp-darwin-x64
4
+ ac153052b5d02b2f19a536623c957369af3412f5bbe2eed0f6a43e8597ed0f1b vsp-darwin-arm64
5
+ 722c6de352719dd9374932b56546189a75c29800e74da3bdcb0308b2034248ac vsp-win32-x64.exe
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.9.9920",
3
+ "version": "0.9.9922",
4
4
  "description": "SAP AI development toolkit for BAS, ABAP, RAP, CAP, HANA Cloud, Fiori, UI5, and MCP",
5
5
  "author": "Gurkan Yilmaz",
6
6
  "type": "module",
@@ -0,0 +1,134 @@
1
+ diff --git a/pkg/adt/edit_namespaced_include_test.go b/pkg/adt/edit_namespaced_include_test.go
2
+ new file mode 100644
3
+ index 0000000..514ab35
4
+ --- /dev/null
5
+ +++ b/pkg/adt/edit_namespaced_include_test.go
6
+ @@ -0,0 +1,80 @@
7
+ +package adt
8
+ +
9
+ +import (
10
+ + "context"
11
+ + "io"
12
+ + "net/http"
13
+ + "net/http/httptest"
14
+ + "strings"
15
+ + "sync"
16
+ + "testing"
17
+ +)
18
+ +
19
+ +// TestEditSource_NamespacedClassInclude guards against double-encoding the
20
+ +// class name of a namespaced class include: the object URL arrives with
21
+ +// "%2FABC%2F", and every request (read, lock, hash check, PUT, unlock,
22
+ +// activate) must keep exactly one encoding — never "%252F".
23
+ +func TestEditSource_NamespacedClassInclude(t *testing.T) {
24
+ + const source = "CLASS lcl_x DEFINITION.\n \" old\nENDCLASS.\n"
25
+ + const classPath = "/sap/bc/adt/oo/classes/%2FABC%2FCL_TEST"
26
+ +
27
+ + for _, include := range []string{"definitions", "implementations", "testclasses"} {
28
+ + for _, withHash := range []bool{false, true} {
29
+ + name := include
30
+ + if withHash {
31
+ + name += "/hash"
32
+ + }
33
+ + t.Run(name, func(t *testing.T) {
34
+ + includePath := classPath + "/includes/" + include
35
+ + var mu sync.Mutex
36
+ + var puts []string
37
+ + current := source
38
+ + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39
+ + w.Header().Set("x-csrf-token", "TOKEN")
40
+ + raw := r.URL.EscapedPath()
41
+ + if strings.Contains(raw, "%25") {
42
+ + t.Errorf("double-encoded request: %s %s", r.Method, r.RequestURI)
43
+ + w.WriteHeader(http.StatusNotFound)
44
+ + return
45
+ + }
46
+ + switch {
47
+ + case r.URL.Query().Get("_action") == "LOCK":
48
+ + w.Header().Set("Content-Type", "application/xml")
49
+ + _, _ = w.Write([]byte(`<?xml version="1.0"?><asx:abap xmlns:asx="http://www.sap.com/abapxml">
50
+ + <asx:values><DATA><LOCK_HANDLE>HANDLE-1</LOCK_HANDLE>
51
+ + <MODIFICATION_SUPPORT>Modification</MODIFICATION_SUPPORT></DATA></asx:values></asx:abap>`))
52
+ + case r.Method == http.MethodGet && raw == includePath:
53
+ + mu.Lock()
54
+ + _, _ = w.Write([]byte(current))
55
+ + mu.Unlock()
56
+ + case r.Method == http.MethodPut:
57
+ + mu.Lock()
58
+ + puts = append(puts, raw)
59
+ + body, _ := io.ReadAll(r.Body)
60
+ + current = string(body)
61
+ + mu.Unlock()
62
+ + default:
63
+ + w.WriteHeader(http.StatusOK)
64
+ + }
65
+ + }))
66
+ + defer srv.Close()
67
+ +
68
+ + opts := &EditSourceOptions{}
69
+ + if withHash {
70
+ + opts.ExpectedSourceHash = SourceHash(source)
71
+ + }
72
+ + c := NewClient(srv.URL, "TESTUSER", "pw")
73
+ + res, err := c.EditSourceWithOptions(context.Background(), includePath, "\" old", "\" new", opts)
74
+ + if err != nil {
75
+ + t.Fatalf("EditSourceWithOptions: %v", err)
76
+ + }
77
+ + if !res.Success {
78
+ + t.Fatalf("edit failed: %s", res.Message)
79
+ + }
80
+ + if len(puts) != 1 || puts[0] != includePath {
81
+ + t.Fatalf("PUT paths = %v, want [%s]", puts, includePath)
82
+ + }
83
+ + })
84
+ + }
85
+ + }
86
+ +}
87
+ diff --git a/pkg/adt/workflows_edit.go b/pkg/adt/workflows_edit.go
88
+ index 6da5df7..01c02e0 100644
89
+ --- a/pkg/adt/workflows_edit.go
90
+ +++ b/pkg/adt/workflows_edit.go
91
+ @@ -3,6 +3,7 @@ package adt
92
+ import (
93
+ "context"
94
+ "fmt"
95
+ + "net/url"
96
+ "strings"
97
+ )
98
+
99
+ @@ -204,9 +205,9 @@ func (c *Client) EditSourceWithOptions(ctx context.Context, objectURL, oldString
100
+ if idx := strings.Index(objectURL, classesPrefix); idx >= 0 {
101
+ rest := objectURL[idx+len(classesPrefix):]
102
+ if slashIdx := strings.Index(rest, "/"); slashIdx > 0 {
103
+ - classNameForMethod = rest[:slashIdx]
104
+ + classNameForMethod = decodeClassNameSegment(rest[:slashIdx])
105
+ } else {
106
+ - classNameForMethod = rest
107
+ + classNameForMethod = decodeClassNameSegment(rest)
108
+ }
109
+ }
110
+ result.Method = opts.Method
111
+ @@ -227,7 +228,7 @@ func (c *Client) EditSourceWithOptions(ctx context.Context, objectURL, oldString
112
+ classesPrefix := "/sap/bc/adt/oo/classes/"
113
+ if strings.Contains(objectURL, classesPrefix) {
114
+ classStart := strings.Index(objectURL, classesPrefix) + len(classesPrefix)
115
+ - className = objectURL[classStart:includesIdx]
116
+ + className = decodeClassNameSegment(objectURL[classStart:includesIdx])
117
+ includeType = ClassIncludeType(objectURL[includesIdx+len("/includes/"):])
118
+ parentClassURL = objectURL[:includesIdx]
119
+ }
120
+ @@ -488,3 +489,14 @@ func (c *Client) EditSourceWithOptions(ctx context.Context, objectURL, oldString
121
+ }
122
+ return result, nil
123
+ }
124
+ +
125
+ +// decodeClassNameSegment turns the class-name path segment of an object URL
126
+ +// ("%2FABC%2FCL_TEST") into the raw name ("/ABC/CL_TEST"). Downstream helpers
127
+ +// (GetClassIncludeSourceURL, GetClassMethods, activation) escape the name
128
+ +// themselves, so passing the still-encoded segment would yield "%252F".
129
+ +func decodeClassNameSegment(segment string) string {
130
+ + if decoded, err := url.PathUnescape(segment); err == nil {
131
+ + return decoded
132
+ + }
133
+ + return segment
134
+ +}
@@ -33,7 +33,9 @@ async function main() {
33
33
  await mkdir(dirname(source), { recursive: true });
34
34
  await run('git', ['clone', '--filter=blob:none', upstream, source]);
35
35
  await run('git', ['-C', source, 'checkout', '--detach', commit]);
36
- await run('patch', ['-p1', '-i', join(root, 'patches', 'vsp-bas-proxy-auth.patch')], { cwd: source });
36
+ for (const patchFile of ['vsp-bas-proxy-auth.patch', 'vsp-namespaced-class-include-edit.patch']) {
37
+ await run('patch', ['-p1', '-i', join(root, 'patches', patchFile)], { cwd: source });
38
+ }
37
39
  await mkdir(dist, { recursive: true });
38
40
  const checksums = [];
39
41
  for (const [goos, goarch, os, arch, extension] of targets) {
@@ -30,7 +30,7 @@ const PROXY_URL = `ZAI_SmokeUrl${RUN}`;
30
30
  const PROXY_ODATA = `ZAI_SmokeOData${RUN}`;
31
31
  const PROXY_CPI = `ZAI_SmokeCpi${RUN}`;
32
32
  const config = resolveIntegrationConfig(process.env, { stored: readStoredIntegrationKeys() });
33
- const SECRETS = Object.values(config).filter(Boolean).map(side => side.clientSecret);
33
+ const SECRETS = Object.values(config).filter(Boolean).map(side => side.clientSecret).filter(Boolean);
34
34
  const PRODUCT = `ZAI_SmokeProduct${RUN}`;
35
35
  const KVM = `ZAI_SmokeKvm${RUN}`;
36
36
  const CREDENTIAL = `ZAI_SMOKE_USER_${RUN}`;
@@ -99,7 +99,7 @@ async function invoke(url, { method = 'POST', body, headers = {}, auth = true, d
99
99
  const text = await response.text();
100
100
  let json;
101
101
  try { json = JSON.parse(text); } catch { /* not JSON */ }
102
- result = { status: response.status, text, json };
102
+ result = { status: response.status, headers: Object.fromEntries(response.headers), text, json };
103
103
  if (done(result)) return result;
104
104
  await sleep(5000);
105
105
  }
@@ -562,7 +562,7 @@ await scenario('S41', 'runtime call to the template iFlow passes body and query
562
562
  expect(/\/anything\/configured/.test(result.json.url), `receiver url ${result.json.url}`);
563
563
  expect(result.json.args.probe === RUN, `query ${JSON.stringify(result.json.args)}`);
564
564
  expect(JSON.parse(result.json.data).kind === 'direct', `body ${result.json.data}`);
565
- ctx.directGuid = result.json.headers['Sap-Messageprocessinglogid'];
565
+ ctx.directGuid = result.headers['sap_messageprocessinglogid'];
566
566
  expect(ctx.directGuid, 'no MPL id header forwarded');
567
567
  return `MPL ${ctx.directGuid}`;
568
568
  });
@@ -579,7 +579,7 @@ await scenario('S43', 'APIM proxy in front of CPI forwards the caller bearer tok
579
579
  const result = await invoke(proxy.runtime_url, { body: JSON.stringify({ via: 'apim-url-target', run: RUN }) });
580
580
  expect(result.status === 200, `HTTP ${result.status}: ${result.text.slice(0, 200)}`);
581
581
  expect(JSON.parse(result.json.data).via === 'apim-url-target', result.json.data);
582
- ctx.apimGuid = result.json.headers['Sap-Messageprocessinglogid'];
582
+ ctx.apimGuid = result.headers['sap_messageprocessinglogid'];
583
583
  return `${proxy.runtime_url} → CPI → httpbin`;
584
584
  });
585
585
 
@@ -611,8 +611,8 @@ await scenario('S46', 'iFlow with Unicode description, POST receiver and static
611
611
  const { runtime } = await deployAndWait(IFLOW_POST);
612
612
  expect(runtime.Status === 'STARTED', JSON.stringify(runtime));
613
613
  const endpoints = ok(await call('cpi_list_service_endpoints', { iflow_id: IFLOW_POST }), 'endpoints').items;
614
- ctx.postEndpoint = endpoints.flatMap(entry => entry.urls)[0] ?? ctx.cpiEndpoint.replace(`/http/ai/smoke${RUN}`, `/http/ai/post${RUN}`);
615
- return `description "${listed.Description}"`;
614
+ ctx.postEndpoint = endpoints.flatMap(entry => entry.urls)[0] ?? ctx.cpiEndpoint?.replace(`/http/ai/smoke${RUN}`, `/http/ai/post${RUN}`);
615
+ expect(ctx.postEndpoint, 'no POST endpoint; run with S18 in ONLY');
616
616
  });
617
617
 
618
618
  await scenario('S47', 'POST receiver gets method, static query and body', async () => {
@@ -914,7 +914,7 @@ await scenario('S79', 'full chain after recovery: client → APIM (provider) →
914
914
  const proxy = ok(await call('apim_get_api_proxy', { name: PROXY_CPI_PROVIDER }), 'get');
915
915
  const result = await invoke(proxy.runtime_url, { body: '{"final":true}', done: response => response.status === 200 && /restored/.test(response.json?.url || '') });
916
916
  expect(result.status === 200 && JSON.parse(result.json.data).final === true, `HTTP ${result.status}`);
917
- const detail = await waitForGuid(result.json.headers['Sap-Messageprocessinglogid']);
917
+ const detail = await waitForGuid(result.headers['sap_messageprocessinglogid']);
918
918
  expect(detail.log.Status === 'COMPLETED', detail.log.Status);
919
919
  });
920
920
 
package/src/setup.mjs CHANGED
@@ -292,18 +292,20 @@ export function integrationSuiteCredentialStatus(env = process.env) {
292
292
  const config = resolveIntegrationConfig(env, { stored: readStoredIntegrationKeys(env) });
293
293
  const keys = ['cpi', 'apim', 'devhub', ...OPTIONAL_COMPONENT_KEYS];
294
294
  const side = (key, value) => {
295
- if (!value) return 'not configured';
296
295
  const host = value.url ? new URL(value.url).host : key === 'aem' ? 'api.solace.cloud' : 'unknown';
297
296
  return `configured from ${value.source}${value.bindingName ? ` (${value.bindingName})` : ''}, host ${host}`;
298
297
  };
299
- return { found: keys.some(key => Boolean(config[key])), config, lines: [
300
- `Environment label: ${config.environment ?? 'default'} (tenant authority remains bound host identity)`,
301
- ...keys.map(key => `${key.toUpperCase()}: ${side(key, config[key])}`),
302
- ...(config.optional_config_errors ?? []).map(error => `${error.component.toUpperCase()}: invalid optional configuration (${error.source}); adapter disabled`)
298
+ const configured = keys.filter(key => config[key]);
299
+ const errors = (config.optional_config_errors ?? []).map(error => `${error.component.toUpperCase()}: invalid optional configuration (${error.source}); adapter disabled`);
300
+ if (!configured.length && !errors.length) return { found: false, config, lines: ['No Integration Suite credentials configured yet.'] };
301
+ return { found: configured.length > 0, config, lines: [
302
+ ...(config.environment ? [`Environment label: ${config.environment}`] : []),
303
+ ...configured.map(key => `${key.toUpperCase()}: ${side(key, config[key])}`),
304
+ ...errors
303
305
  ] };
304
306
  } catch (error) {
305
307
  const message = String(error?.message || '');
306
- if (/^Configure CPI/.test(message)) return { found: false, config: null, lines: ['No Integration Suite component credentials were found in this environment.'] };
308
+ if (/^Configure CPI/.test(message)) return { found: false, config: null, lines: ['No Integration Suite credentials configured yet.'] };
307
309
  return { found: false, config: null, lines: [`Credentials found but not usable: ${message}`] };
308
310
  }
309
311
  }
@@ -330,7 +332,7 @@ function pastedServiceKey(side, value, env) {
330
332
  }
331
333
 
332
334
  async function printIntegrationVerification(config, { env, output, verifyIntegration }) {
333
- const stop = startProgress('Checking configured core and optional Integration Suite access', { output, env, label: 'Integration Suite' });
335
+ const stop = startProgress('Checking Integration Suite access', { output, env, label: 'Integration Suite' });
334
336
  let report;
335
337
  try { report = await verifyIntegration(config, { env }); }
336
338
  finally { stop(); }
@@ -344,12 +346,12 @@ async function printIntegrationVerification(config, { env, output, verifyIntegra
344
346
  }
345
347
  const core = [report.cpi, report.apim, report.devhub].filter(side => side?.configured);
346
348
  const denied = core.flatMap(side => side.checks ?? []).filter(check => check.result === 'forbidden').length;
347
- const message = !core.length ? 'No core clients configured; optional checks are reported separately.'
348
- : report.ok ? `Configured core access verified${denied ? `; ${denied} capability checks were access-denied (cause not established)` : ''}.`
349
- : 'Configured core reads did not pass. Inspect the individual access evidence; a role or entitlement diagnosis is not established.';
349
+ const message = !core.length ? 'No core service configured yet.'
350
+ : report.ok ? (denied ? `Connected; ${denied} area(s) were access-denied — the key may lack roles.` : 'Connection verified.')
351
+ : 'Sign-in or core read failed; see the lines above.';
350
352
  print(output, formatStatus(message, report.ok && !denied ? 'success' : 'warning', output, 'Integration Suite'));
351
353
  const optional = OPTIONAL_COMPONENT_KEYS.filter(side => report[side]?.configured);
352
- if (optional.length) print(output, ` Optional capability completeness: ${report.optional_completeness?.complete === true ? 'complete' : 'unverified/partial'}; core access is a separate result.`);
354
+ if (optional.length) print(output, ` Optional services: ${report.optional_completeness?.complete === true ? 'all verified' : 'not fully verified'} (separate from core access).`);
353
355
  return report;
354
356
  }
355
357
 
@@ -470,6 +472,26 @@ async function useCloudFoundrySpace(target, context) {
470
472
  }
471
473
  }
472
474
 
475
+ // The paste flow asks which service the key belongs to; APIM and Developer Hub keys share one OAuth shape,
476
+ // so the type is never guessed from the key itself.
477
+ async function chooseKeyType({ input, output, status }) {
478
+ return await selectPrompt({
479
+ message: colorText('Which service is this key for?', 'cyan', output),
480
+ defaultValue: ['CPI', 'APIM', 'DEVHUB'].find(side => !status.config?.[side.toLowerCase()]) ?? 'CPI',
481
+ choices: [
482
+ { value: 'CPI', shortcut: 'i', name: 'Cloud Integration (CPI) — Process Integration Runtime, plan api' },
483
+ { value: 'APIM', shortcut: 'a', name: 'API Management — plan apiportal-apiaccess' },
484
+ { value: 'DEVHUB', shortcut: 'd', name: 'Developer Hub — plan devportal-apiaccess (AuthGroup.API.Admin)' },
485
+ ...OPTIONAL_COMPONENT_KEYS.map(side => ({
486
+ value: side.toUpperCase(),
487
+ shortcut: { ctms: 't', ans: 'n', event_mesh: 'e', aem: 'm', aem_semp: 's', open_connectors: 'o', edge: 'g' }[side],
488
+ name: `Optional: ${OPTIONAL_INPUTS[side].label} (${OPTIONAL_INPUTS[side].variable}, existing credentials only)`
489
+ })),
490
+ { value: '', shortcut: 'b', name: 'Back' }
491
+ ]
492
+ }, { input, output });
493
+ }
494
+
473
495
  // Verify the detected credentials online and let the user take CPI/APIM/DEVHUB keys from the logged-in CF space or
474
496
  // paste them. A key is checked online first and saved to the user-only store (never to mcp.json); it is used
475
497
  // when the environment has no key.
@@ -502,20 +524,13 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
502
524
  : [];
503
525
  const choice = await selectPrompt({
504
526
  message: colorText('🔑 Integration Suite credentials', 'cyan', output),
505
- defaultValue: healthy ? 'done' : (fixes[0]?.value ?? (target.available ? 'cf' : (status.found ? 'done' : 'cpi'))),
527
+ defaultValue: healthy ? 'done' : (fixes[0]?.value ?? (target.available ? 'cf' : (status.found ? 'done' : 'paste'))),
506
528
  choices: [
507
529
  ...fixes,
508
- { name: !status.found ? 'Continue without credentials (operational tools need configured component access)' : (healthy ? 'Continue' : 'Continue with current access (unavailable capabilities remain unverified)'), value: 'done', shortcut: 'c' },
509
- ...(target.available ? [{ name: `Use the logged-in CF space (${space}): find instances, reuse or create keys`, value: 'cf', shortcut: 'f' }] : []),
510
- { name: 'Paste a Cloud Integration (CPI) service key — Process Integration Runtime, plan api', value: 'cpi', shortcut: 'p' },
511
- { name: 'Paste an API Management service key — API Management, plan apiportal-apiaccess', value: 'apim', shortcut: 'a' },
512
- { name: 'Paste a Developer Hub service key — API Management, Developer Hub, plan devportal-apiaccess (AuthGroup.API.Admin)', value: 'devhub', shortcut: 'd' },
513
- ...OPTIONAL_COMPONENT_KEYS.map(side => ({
514
- name: `Paste existing ${OPTIONAL_INPUTS[side].label} configuration — ${OPTIONAL_INPUTS[side].variable} (no provisioning)`,
515
- value: `optional-${side}`,
516
- shortcut: { ctms: 't', ans: 'n', event_mesh: 'e', aem: 'm', aem_semp: 's', open_connectors: 'o', edge: 'g' }[side]
517
- })),
518
- ...(status.found ? [{ name: 'Check the configured credentials online again', value: 'verify', shortcut: 'v' }] : [])
530
+ ...(target.available ? [{ value: 'cf', shortcut: 'f', name: `Get keys from Cloud Foundry (${space})` }] : []),
531
+ { value: 'paste', shortcut: 'p', name: 'Paste a service key (JSON or path to a .json file)' },
532
+ ...(status.found ? [{ value: 'verify', shortcut: 'v', name: 'Check credentials again' }] : []),
533
+ { value: 'done', shortcut: 'c', name: status.found ? (healthy ? 'Done' : 'Done (some checks did not pass)') : 'Skip for now (run sap-ai-dev --setup-integration later)' }
519
534
  ]
520
535
  }, { input, output });
521
536
  if (choice === 'done') return status;
@@ -527,8 +542,9 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
527
542
  await createDedicatedInstance(choice.slice('create-'.length).toUpperCase(), context);
528
543
  } else if (choice === 'cf') {
529
544
  await useCloudFoundrySpace(target, context);
530
- } else {
531
- const side = (choice.startsWith('optional-') ? choice.slice('optional-'.length) : choice).toUpperCase();
545
+ } else if (choice === 'paste') {
546
+ const side = await chooseKeyType({ input, output, status });
547
+ if (!side) continue; // Back: show the menu again without refreshing status.
532
548
  const pasted = await textPrompt({
533
549
  message: `Paste existing ${side} credential JSON or the path to its .json file`,
534
550
  secret: true,
@@ -550,16 +566,12 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
550
566
 
551
567
  async function promptIntegrationSuite({ env, alreadyConfigured, input, output, verifyIntegration = defaultVerifyIntegration, integrationCloudFoundry, ask = true }) {
552
568
  print(output, '');
553
- print(output, formatStatus('SAP Integration Suite tools (core lifecycle and separately credentialed optional services).', 'step', output, 'Integration Suite'));
554
- print(output, ' Adds the sap-ai-integration MCP server: author/configure/release, bounded monitoring and diagnosis, guarded recovery/retirement, and evidence-backed optional/native workflows.');
555
- print(output, ' Writes require exact approval; private previews are not approval. Optional setup accepts existing credentials only, without provisioning. Agents: SAP Integration Architect, SAP Integration Developer.');
569
+ print(output, formatStatus('SAP Integration Suite tools', 'step', output, 'Integration Suite'));
570
+ print(output, ' Connects your AI agent to Cloud Integration (CPI), API Management and Developer Hub. Keys are checked read-only and stored only for you, never in mcp.json.');
556
571
  if (alreadyConfigured) print(output, ' Already configured in this MCP config; keeping it and refreshing its version.');
557
572
  const status = integrationSuiteCredentialStatus(env);
558
573
  for (const line of status.lines) print(output, ` ${line}`);
559
- if (!status.found) {
560
- print(output, ' Configure existing core or optional component credentials in the MCP host environment,');
561
- print(output, ' or paste typed credential JSON in the next hidden step. Credentials are verified read-only and stored user-only, never in mcp.json.');
562
- }
574
+ if (!status.found) print(output, ' No credentials found yet. Next you can get them from your Cloud Foundry space or paste a service key.');
563
575
  if (ask && !alreadyConfigured) {
564
576
  print(output, '');
565
577
  const defaultYes = status.found;