sap-ai-dev-toolkit 0.2.7 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/binary.mjs CHANGED
@@ -3,6 +3,7 @@ import { chmod, mkdir, rename, stat, writeFile } from 'node:fs/promises';
3
3
  import { homedir, tmpdir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { brandedEnvValue } from './branding.mjs';
6
7
 
7
8
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
8
9
  export const UPSTREAM_COMMIT = '9886d2727f47506368b0a3c2f1c1766f1200f747';
@@ -17,7 +18,7 @@ const platformMap = {
17
18
 
18
19
  export function binaryTarget(platform = process.platform, arch = process.arch) {
19
20
  const target = platformMap[`${platform}:${arch}`];
20
- if (!target) throw new Error(`Unsupported platform ${platform}/${arch}. Set BAS_VSP_BINARY or build cmd/vsp with scripts/build-vsp.mjs.`);
21
+ if (!target) throw new Error(`Unsupported platform ${platform}/${arch}. Set SAP_AI_DEV_TOOLKIT_BINARY or build cmd/vsp with scripts/build-vsp.mjs.`);
21
22
  return { os: target[0], arch: target[1], extension: target[2], asset: `vsp-${target[0]}-${target[1]}${target[2]}` };
22
23
  }
23
24
 
@@ -32,7 +33,7 @@ export function packageRepositoryUrl(pkg) {
32
33
  }
33
34
 
34
35
  export function releaseBaseUrl(pkg, version = pkg.version) {
35
- const override = process.env.BAS_VSP_RELEASE_BASE_URL;
36
+ const override = brandedEnvValue(process.env, 'RELEASE_BASE_URL');
36
37
  if (override) return `${override.replace(/\/$/, '')}/v${version}`;
37
38
  const repo = packageRepositoryUrl(pkg).replace(/^ssh:\/\/git@github.com:/, 'https://github.com/').replace(/^git@github.com:/, 'https://github.com/');
38
39
  if (!repo.startsWith('http')) throw new Error('package repository URL must be an HTTPS URL for release assets');
@@ -43,48 +44,57 @@ export function cacheDirectory() {
43
44
  return process.env.BAS_VSP_CACHE_DIR || join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'sap-ai-dev-toolkit');
44
45
  }
45
46
 
46
- export function cachedBinaryPath(pkg, platform = process.platform, arch = process.arch) {
47
+ export function cachedBinaryPath(pkg, platform = process.platform, arch = process.arch, env = process.env) {
47
48
  const target = binaryTarget(platform, arch);
48
- return join(cacheDirectory(), `${pkg.name}-${pkg.version}-${target.asset}`);
49
+ return join(cacheDirectory(env), `${pkg.name}-${pkg.version}-${target.asset}`);
50
+ }
51
+
52
+ function legacyCachedBinaryPath(pkg, platform, arch, env) {
53
+ const target = binaryTarget(platform, arch);
54
+ const configuredCache = brandedEnvValue(env, 'CACHE_DIR');
55
+ const cacheRoot = configuredCache || join(env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'bas-mcp-addon');
56
+ return join(cacheRoot, `bas-mcp-addon-${pkg.version}-${target.asset}`);
49
57
  }
50
58
 
51
59
  export async function findBinary(pkg, options = {}) {
52
60
  const env = options.env || process.env;
53
- if (env.BAS_VSP_BINARY) return env.BAS_VSP_BINARY;
54
- if (!env.BAS_VSP_BINARY_URL) {
61
+ if (brandedEnvValue(env, 'BINARY')) return brandedEnvValue(env, 'BINARY');
62
+ if (!brandedEnvValue(env, 'BINARY_URL')) {
55
63
  const bundled = bundledBinaryPath(options.platform, options.arch);
56
64
  try { await stat(bundled); return bundled; } catch {}
57
65
  }
58
- const path = cachedBinaryPath(pkg, options.platform, options.arch);
59
- try { await stat(path); return path; } catch { return null; }
66
+ const path = cachedBinaryPath(pkg, options.platform, options.arch, env);
67
+ try { await stat(path); return path; } catch {}
68
+ const legacyPath = legacyCachedBinaryPath(pkg, options.platform, options.arch, env);
69
+ try { await stat(legacyPath); return legacyPath; } catch { return null; }
60
70
  }
61
71
 
62
72
  export async function installBinary(pkg, options = {}) {
63
73
  const env = options.env || process.env;
64
- if (env.BAS_VSP_BINARY) return env.BAS_VSP_BINARY;
65
- if (!env.BAS_VSP_BINARY_URL) {
74
+ if (brandedEnvValue(env, 'BINARY')) return brandedEnvValue(env, 'BINARY');
75
+ if (!brandedEnvValue(env, 'BINARY_URL')) {
66
76
  const bundled = bundledBinaryPath(options.platform, options.arch);
67
77
  try { await stat(bundled); return bundled; } catch {}
68
78
  }
69
79
  const target = binaryTarget(options.platform, options.arch);
70
80
  const base = releaseBaseUrl(pkg, pkg.version);
71
- const assetUrl = env.BAS_VSP_BINARY_URL || `${base}/${target.asset}`;
81
+ const assetUrl = brandedEnvValue(env, 'BINARY_URL') || `${base}/${target.asset}`;
72
82
  const checksumUrl = `${base}/checksums.txt`;
73
83
  const response = await fetch(assetUrl);
74
- if (!response.ok) throw new Error(`VSP binary download failed (${response.status}); set BAS_VSP_BINARY or BAS_VSP_BINARY_URL`);
84
+ if (!response.ok) throw new Error(`VSP binary download failed (${response.status}); set SAP_AI_DEV_TOOLKIT_BINARY or SAP_AI_DEV_TOOLKIT_BINARY_URL`);
75
85
  const bytes = Buffer.from(await response.arrayBuffer());
76
86
  const checksumResponse = await fetch(checksumUrl);
77
- if (!checksumResponse.ok) throw new Error(`VSP checksum download failed (${checksumResponse.status}); set BAS_VSP_BINARY or BAS_VSP_BINARY_URL`);
87
+ if (!checksumResponse.ok) throw new Error(`VSP checksum download failed (${checksumResponse.status}); set SAP_AI_DEV_TOOLKIT_BINARY or SAP_AI_DEV_TOOLKIT_BINARY_URL`);
78
88
  const checksumText = await checksumResponse.text();
79
89
  const expected = checksumText.split(/\r?\n/).map(line => line.trim().split(/\s+/)).find(parts => parts.length >= 2 && (parts[1] === target.asset || parts[1].endsWith(`/${target.asset}`)))?.[0];
80
- if (!expected) throw new Error(`checksums.txt has no entry for ${target.asset}; set BAS_VSP_BINARY or BAS_VSP_BINARY_URL`);
90
+ if (!expected) throw new Error(`checksums.txt has no entry for ${target.asset}; set SAP_AI_DEV_TOOLKIT_BINARY or SAP_AI_DEV_TOOLKIT_BINARY_URL`);
81
91
  const actual = createHash('sha256').update(bytes).digest('hex');
82
92
  if (actual.toLowerCase() !== expected.toLowerCase()) throw new Error(`VSP checksum mismatch for ${target.asset}`);
83
- const dir = cacheDirectory();
93
+ const dir = cacheDirectory(env);
84
94
  await mkdir(dir, { recursive: true, mode: 0o700 });
85
95
  await chmod(dir, 0o700);
86
- const destination = cachedBinaryPath(pkg, options.platform, options.arch);
87
- const temporary = join(tmpdir(), `bas-vsp-${process.pid}-${Date.now()}`);
96
+ const destination = cachedBinaryPath(pkg, options.platform, options.arch, env);
97
+ const temporary = join(tmpdir(), `sap-ai-dev-toolkit-vsp-${process.pid}-${Date.now()}`);
88
98
  await writeFile(temporary, bytes, { mode: 0o700 });
89
99
  await chmod(temporary, 0o700);
90
100
  await rename(temporary, destination);
@@ -0,0 +1,21 @@
1
+ export const PRODUCT_NAME = 'sap-ai-dev-toolkit';
2
+ export const ENV_PREFIX = 'SAP_AI_DEV_TOOLKIT_';
3
+
4
+ const LEGACY_ENV_PREFIX = 'BAS_VSP_';
5
+
6
+ export function brandedEnvValue(env, suffix) {
7
+ const name = suffix.startsWith(ENV_PREFIX) ? suffix.slice(ENV_PREFIX.length) : suffix;
8
+ const currentKey = `${ENV_PREFIX}${name}`;
9
+ const legacyKey = `${LEGACY_ENV_PREFIX}${name}`;
10
+ return env?.[currentKey] !== undefined ? env[currentKey] : env?.[legacyKey];
11
+ }
12
+
13
+ export function withBrandedEnvironment(env = process.env) {
14
+ const result = { ...env };
15
+ for (const [key, value] of Object.entries(env)) {
16
+ if (!key.startsWith(LEGACY_ENV_PREFIX)) continue;
17
+ const currentKey = `${ENV_PREFIX}${key.slice(LEGACY_ENV_PREFIX.length)}`;
18
+ if (result[currentKey] === undefined) result[currentKey] = value;
19
+ }
20
+ return result;
21
+ }
@@ -695,7 +695,7 @@ export async function resolveConfiguredCloudFoundryDestination({ env = process.e
695
695
  const destinationInstanceGuid = configuredValue(env, 'BAS_CF_DESTINATION_INSTANCE_GUID');
696
696
  const destinationInstanceName = configuredValue(env, 'BAS_CF_DESTINATION_INSTANCE');
697
697
  const destinationKeyName = configuredValue(env, 'BAS_CF_DESTINATION_KEY');
698
- const name = configuredValue(env, 'BAS_CF_DESTINATION_NAME') || configuredValue(env, 'BAS_VSP_DESTINATION');
698
+ const name = configuredValue(env, 'BAS_CF_DESTINATION_NAME') || text(brandedEnvValue(env, 'DESTINATION'));
699
699
  if (!spaceGuid || !destinationInstanceGuid || !destinationInstanceName || !destinationKeyName || !name) {
700
700
  throw new Error('Cloud Foundry destination configuration is incomplete; rerun sap-ai-dev --setup.');
701
701
  }
@@ -0,0 +1,113 @@
1
+ const objects = new Map([
2
+ ['CLAS:ZCL_DEMO_ORDER', `CLASS zcl_demo_order DEFINITION PUBLIC FINAL CREATE PUBLIC.\n PUBLIC SECTION.\n METHODS get_status IMPORTING order_id TYPE string RETURNING VALUE(status) TYPE string.\nENDCLASS.\n\nCLASS zcl_demo_order IMPLEMENTATION.\n METHOD get_status.\n status = |Order { order_id } is ready|.\n ENDMETHOD.\nENDCLASS.`],
3
+ ['DDLS:ZI_DEMO_ORDER', `@EndUserText.label: 'Demo Orders'\ndefine view entity ZI_DEMO_ORDER\n as select from zdemo_order\n{\n key order_id as OrderId,\n description as Description\n}`]
4
+ ]);
5
+
6
+ const transports = new Map([
7
+ ['DEMO000001', { status: 'modifiable', description: 'Offline demo transport', objects: ['ZCL_DEMO_ORDER', 'ZI_DEMO_ORDER'] }]
8
+ ]);
9
+ const tools = [
10
+ ['GetSource', { object_type: 'string', name: 'string' }, ['object_type', 'name']],
11
+ ['WriteSource', { object_type: 'string', name: 'string', source: 'string' }, ['object_type', 'name', 'source']],
12
+ ['SearchObject', { query: 'string' }, ['query']],
13
+ ['GetSystemInfo', {}, []],
14
+ ['GetFeatures', {}, []],
15
+ ['GetConnectionInfo', {}, []],
16
+ ['GetAPIReleaseState', { object_uri: 'string' }, ['object_uri']],
17
+ ['GetTransport', { transport: 'string' }, ['transport']],
18
+ ['ListTransports', {}, []],
19
+ ['GetTransportInfo', { object_url: 'string', dev_class: 'string' }, ['object_url', 'dev_class']],
20
+ ['ListDependencies', { object_uri: 'string' }, ['object_uri']],
21
+ ['GetInactiveObjects', {}, []],
22
+ ['RunUnitTests', { object_name: 'string' }, ['object_name']],
23
+ ['RunATCCheck', { object_name: 'string' }, ['object_name']],
24
+ ['SyntaxCheck', { object_type: 'string', name: 'string', source: 'string' }, ['object_type', 'name', 'source']],
25
+ ['CreateTransport', { description: 'string' }, ['description']]
26
+ ].map(([name, properties, required]) => ({
27
+ name,
28
+ description: `Offline demo fixture for ${name}. No live SAP system is contacted.`,
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: Object.fromEntries(Object.entries(properties).map(([key, type]) => [key, { type }])),
32
+ required,
33
+ additionalProperties: false
34
+ }
35
+ }));
36
+
37
+ function send(id, result) {
38
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
39
+ }
40
+
41
+ function toolResult(value) {
42
+ return { isError: false, content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value };
43
+ }
44
+
45
+ function errorResult(message) {
46
+ return { isError: true, content: [{ type: 'text', text: message }] };
47
+ }
48
+
49
+ function key(args) {
50
+ return `${String(args.object_type || '').toUpperCase()}:${String(args.name || '').toUpperCase()}`;
51
+ }
52
+
53
+ function getSource(args) {
54
+ const source = objects.get(key(args));
55
+ if (source === undefined) return errorResult(`Object ${args.object_type} ${args.name} is not in the demo fixture.`);
56
+ return { isError: false, content: [{ type: 'text', text: source }], structuredContent: { source } };
57
+ }
58
+
59
+ function callTool(name, args = {}) {
60
+ if (name === 'GetSource') return getSource(args);
61
+ if (name === 'WriteSource') {
62
+ objects.set(key(args), args.source);
63
+ return toolResult({ status: 'saved-in-memory', object_type: args.object_type, name: args.name });
64
+ }
65
+ if (name === 'SearchObject') {
66
+ const query = String(args.query || '').toUpperCase();
67
+ return toolResult({ objects: [...objects.keys()].filter(value => value.includes(query)).map(value => ({ name: value.split(':')[1], uri: `/sap/bc/adt/oo/classes/${value.split(':')[1].toLowerCase()}` })) });
68
+ }
69
+ if (name === 'GetSystemInfo') return toolResult({ system_id: 'DEMO', release: 'ABAP Platform 2023', database: 'Offline fixture' });
70
+ if (name === 'GetFeatures') return toolResult({ abap_cloud: true, rap_odata: true, cts_transports: true, offline_demo: true });
71
+ if (name === 'GetConnectionInfo') return toolResult({ destination: 'demo', connection: 'offline fixture', live_sap: false });
72
+ if (name === 'GetAPIReleaseState') {
73
+ return toolResult({ object_uri: args.object_uri, release_state: 'not_released', evidence: 'Synthetic API release-state example only.' });
74
+ }
75
+ if (name === 'GetTransport') return transports.has(args.transport) ? toolResult(transports.get(args.transport)) : errorResult(`Transport ${args.transport} is not in the demo fixture.`);
76
+ if (name === 'ListTransports') return toolResult({ transports: [...transports.entries()].map(([transport, details]) => ({ transport, ...details })) });
77
+ if (name === 'GetTransportInfo') return toolResult({ object_url: args.object_url, dev_class: args.dev_class, eligible_requests: ['DEMO000001'], lock_status: 'unlocked' });
78
+ if (name === 'ListDependencies') return toolResult({ dependencies: [], object_uri: args.object_uri, note: 'No additional dependencies in this fixture.' });
79
+ if (name === 'GetInactiveObjects') return toolResult({ objects: [] });
80
+ if (name === 'RunUnitTests') return toolResult({ object_name: args.object_name, status: 'passed', passed: 3, failed: 0, duration_ms: 18 });
81
+ if (name === 'RunATCCheck') return toolResult({ object_name: args.object_name, status: 'passed', findings: [] });
82
+ if (name === 'SyntaxCheck') return toolResult({ status: 'clean', errors: 0, warnings: 0 });
83
+ if (name === 'CreateTransport') {
84
+ const transport = `DEMO${String(transports.size + 1).padStart(6, '0')}`;
85
+ const result = { status: 'created-in-memory', transport, description: args.description };
86
+ transports.set(transport, result);
87
+ return toolResult(result);
88
+ }
89
+ return errorResult(`Tool ${name} is not implemented by the offline demo.`);
90
+ }
91
+
92
+ let buffer = '';
93
+ process.stdin.setEncoding('utf8');
94
+ process.stdin.on('data', chunk => {
95
+ buffer += chunk;
96
+ let newline;
97
+ while ((newline = buffer.indexOf('\n')) >= 0) {
98
+ const line = buffer.slice(0, newline).trim();
99
+ buffer = buffer.slice(newline + 1);
100
+ if (!line) continue;
101
+ let message;
102
+ try { message = JSON.parse(line); } catch { continue; }
103
+ if (message.method === 'initialize') {
104
+ send(message.id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'sap-ai-dev-toolkit-offline-demo', version: '1.0.0' } });
105
+ } else if (message.method === 'tools/list') {
106
+ send(message.id, { tools });
107
+ } else if (message.method === 'tools/call') {
108
+ send(message.id, callTool(message.params?.name, message.params?.arguments));
109
+ } else if (message.method && message.id !== undefined) {
110
+ send(message.id, {});
111
+ }
112
+ }
113
+ });