@nlxai/cli 1.2.2-alpha.2

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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2020, NLX Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # `@nlxai/cli`
2
+
3
+ > Tools for integrating with NLX apps
4
+
5
+ **Warning:** This is an alpha package and will likely not work for you. Keep an eye on this space for a more feature complete release later.
6
+
7
+ ## Usage
8
+
9
+ ```
10
+ > npm -g i cli
11
+
12
+ > nlx
13
+
14
+ Usage: nlx [options] [command]
15
+
16
+ Intereact with NLX from the command line
17
+
18
+ Options:
19
+ -h, --help display help for command
20
+
21
+ Commands:
22
+ auth Authentication and user management
23
+ auth login Authenticate with NLX
24
+ auth logout Clear stored authentication tokens
25
+ auth switch Switch to the next saved NLX account
26
+ auth whoami Show current team, role, and available applications count (requires login)
27
+ modalities Work with NLX modalities
28
+ modalities generate [options] Fetch modalities and generate TypeScript interfaces
29
+ modalities check [file] Type check local TypeScript definitions against server schemas
30
+ data-requests Data Requests
31
+ data-requests sync [opts] <input> Sync Data Requests from an OpenAPI Specification or Swagger
32
+ test [options] <applicationId> Run conversation tests for a given application ID
33
+ http [options] <method> <path> [body] Perform an authenticated request to the management API
34
+ help [command] display help for command
35
+
36
+ > nlx auth login
37
+ Please visit https://nlxdev.us.auth0.com/activate?user_code=JCVM-MQHX and enter code: JCVM-MQHX
38
+ Login successful! Access token stored securely.
39
+
40
+ > nlx modalities generate
41
+ ✔ TypeScript interfaces written to /Users/jakub.hampl/Programming/nlx/sdk/packages/cli/modalities-types.d.ts
42
+
43
+ > nlx modalities check
44
+ ✔ Type check passed: all remote types are assignable to local types.
45
+
46
+ > echo "export interface Foo { bar: string; }" >> modalities-types.d.ts
47
+
48
+ > nlx modalities check
49
+
50
+ ERROR Type check failed:
51
+ - Type/interface 'Foo' does not correspond to any model on the server.
52
+
53
+ > bin/nlx data-requests sync __tests__/input-files/sample-openapi.yaml --dry-run --folder OpenAPI
54
+ ℹ Would create new data request CreateTest POST https://api.example.com/test, but skipping because --dry-run.
55
+
56
+ > bin/nlx data-requests sync __tests__/input-files/sample-openapi.yaml --interactive --folder OpenAPI
57
+ # Interactive session where individual requests can be created or updated starts
58
+
59
+ > bin/nlx test 9bf7404f-8636-4cc6-a33f-cb72ba6a062d --enterprise-region EU
60
+ Fetched 3 tests. Running...
61
+
62
+ ERROR BadTest failed: 0/1
63
+
64
+ ✔ SimpleDataRequest 01
65
+ ✔ SimpleCarousel 02
66
+ --------------------------------
67
+ 1 tests failed
68
+
69
+ ERROR Test: BadTest
70
+
71
+ Assertions met: 0/1
72
+ Transcript:
73
+ 1. User: SimpleCarousel
74
+ 2. Bot:
75
+ ┌─────────────────────────────────────┐
76
+ │Which invoice would you like to view?│
77
+ └─────────────────────────────────────┘
78
+
79
+ 3. User: ➡️ Invoice 001
80
+ 4. Bot:
81
+ ┌───────┐
82
+ │Success│
83
+ └───────┘
84
+
85
+ Debug at: https://dev.platform.nlx.ai/flows/SimpleCarousel
86
+ ```
package/bin/nlx ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ // eslint-disable-next-line no-unused-expressions
6
+ import("../lib/index.js");
@@ -0,0 +1,11 @@
1
+ import { Command } from "commander";
2
+ import { loginCommand } from "./login.js";
3
+ import { logoutCommand } from "./logout.js";
4
+ import { authSwitchCommand } from "./switch.js";
5
+ import { whoamiCommand } from "./whoami.js";
6
+ export const authCommand = new Command("auth")
7
+ .description("Authentication and user management")
8
+ .addCommand(loginCommand)
9
+ .addCommand(logoutCommand)
10
+ .addCommand(authSwitchCommand)
11
+ .addCommand(whoamiCommand);
@@ -0,0 +1,145 @@
1
+ import { Command } from "commander";
2
+ import * as fs from "fs";
3
+ import open from "open";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ import { consola } from "consola";
7
+ import keytar from "keytar";
8
+ export const ACCOUNTS_PATH = path.join(os.homedir(), ".nlx-cli-auth.json");
9
+ async function saveTokens(account, tokenData) {
10
+ await keytar.setPassword("nlx-cli", account, JSON.stringify(tokenData));
11
+ }
12
+ async function loadTokens() {
13
+ try {
14
+ const data = fs.readFileSync(ACCOUNTS_PATH, "utf8");
15
+ const accounts = JSON.parse(data);
16
+ if (accounts.currentAccount) {
17
+ const res = await keytar.getPassword("nlx-cli", accounts.currentAccount);
18
+ if (res)
19
+ return [accounts.currentAccount, JSON.parse(res)];
20
+ }
21
+ throw new Error("No tokens found for current account");
22
+ }
23
+ catch {
24
+ throw new Error("Failed to load tokens");
25
+ }
26
+ }
27
+ async function refreshTokenIfNeeded() {
28
+ let account, tokens;
29
+ try {
30
+ [account, tokens] = await loadTokens();
31
+ }
32
+ catch (error) {
33
+ consola.error("Error loading tokens");
34
+ return null;
35
+ }
36
+ if (!tokens || !tokens.refresh_token)
37
+ return null;
38
+ // Check expiry
39
+ const now = Math.floor(Date.now() / 1000);
40
+ if (tokens.expires_in &&
41
+ tokens.obtained_at &&
42
+ now < tokens.obtained_at + tokens.expires_in - 60) {
43
+ consola.debug("Access token is still valid.");
44
+ return tokens.access_token;
45
+ }
46
+ consola.debug("Access token is expired or invalid. Refreshing...");
47
+ // Refresh
48
+ const res = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
51
+ body: new URLSearchParams({
52
+ grant_type: "refresh_token",
53
+ client_id: CLIENT_ID,
54
+ refresh_token: tokens.refresh_token,
55
+ }),
56
+ });
57
+ const newTokens = await res.json();
58
+ if (newTokens.access_token) {
59
+ newTokens.refresh_token = newTokens.refresh_token || tokens.refresh_token;
60
+ newTokens.obtained_at = now;
61
+ await saveTokens(account, newTokens);
62
+ return newTokens.access_token;
63
+ }
64
+ return null;
65
+ }
66
+ const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN || "nlxdev.us.auth0.com"; // e.g. 'dev-xxxxxx.us.auth0.com'
67
+ const CLIENT_ID = process.env.AUTH0_CLIENT_ID || "A0qluq7wJQjFjMLle9pvrWWaVHM1QHE3";
68
+ const AUDIENCE = process.env.AUTH0_AUDIENCE || "https://nlxdev.us.auth0.com/api/v2/";
69
+ export const loginCommand = new Command("login")
70
+ .description("Authenticate with NLX")
71
+ .action(async () => {
72
+ // Step 1: Start device flow
73
+ const deviceCodeRes = await fetch(`https://${AUTH0_DOMAIN}/oauth/device/code`, {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
76
+ body: new URLSearchParams({
77
+ client_id: CLIENT_ID,
78
+ scope: "openid profile email offline_access",
79
+ audience: AUDIENCE,
80
+ }),
81
+ });
82
+ const deviceCodeData = await deviceCodeRes.json();
83
+ open(deviceCodeData.verification_uri_complete);
84
+ consola.box(`Please visit ${deviceCodeData.verification_uri_complete} and enter code: ${deviceCodeData.user_code}`);
85
+ // Step 2: Poll for token
86
+ let tokenData;
87
+ while (!tokenData) {
88
+ await new Promise((r) => setTimeout(r, deviceCodeData.interval * 1000));
89
+ const tokenRes = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
92
+ body: new URLSearchParams({
93
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
94
+ device_code: deviceCodeData.device_code,
95
+ client_id: CLIENT_ID,
96
+ }),
97
+ });
98
+ const resData = await tokenRes.json();
99
+ if (resData.access_token) {
100
+ tokenData = resData;
101
+ }
102
+ else if (resData.error !== "authorization_pending") {
103
+ consola.error("Error:", resData.error_description || resData.error);
104
+ return;
105
+ }
106
+ }
107
+ // Step 3: Fetch user object
108
+ let accounts = { currentAccount: null, accounts: [] };
109
+ if (fs.existsSync(ACCOUNTS_PATH)) {
110
+ const data = fs.readFileSync(ACCOUNTS_PATH, "utf8");
111
+ accounts = JSON.parse(data);
112
+ }
113
+ const userRes = await fetch(`https://${AUTH0_DOMAIN}/userinfo`, {
114
+ headers: {
115
+ Authorization: `Bearer ${tokenData.access_token}`,
116
+ },
117
+ });
118
+ const userData = await userRes.json();
119
+ if (!accounts.currentAccount) {
120
+ await fs.promises.writeFile(ACCOUNTS_PATH, JSON.stringify({
121
+ currentAccount: userData.email,
122
+ accounts: [userData.email],
123
+ }));
124
+ }
125
+ else if (accounts.currentAccount !== userData.email) {
126
+ accounts.currentAccount = userData.email;
127
+ await fs.promises.writeFile(ACCOUNTS_PATH, JSON.stringify({
128
+ currentAccount: userData.email,
129
+ accounts: [userData.email, ...accounts.accounts],
130
+ }));
131
+ }
132
+ // Step 4: Store token securely
133
+ tokenData.obtained_at = Math.floor(Date.now() / 1000);
134
+ await saveTokens(userData.email, tokenData);
135
+ consola.success("Login successful! Access token stored securely.");
136
+ });
137
+ // Example usage: get a valid access token
138
+ export async function ensureToken() {
139
+ const accessToken = await refreshTokenIfNeeded();
140
+ if (!accessToken) {
141
+ consola.error("Not authenticated. Please run 'login' first.");
142
+ process.exit(1);
143
+ }
144
+ return accessToken;
145
+ }
@@ -0,0 +1,32 @@
1
+ import { Command } from "commander";
2
+ import { consola } from "consola";
3
+ import { ACCOUNTS_PATH } from "./login.js";
4
+ import keytar from "keytar";
5
+ import * as fs from "fs";
6
+ export const logoutCommand = new Command("logout")
7
+ .description("Clear stored authentication tokens")
8
+ .action(async () => {
9
+ try {
10
+ let accounts = { currentAccount: null, accounts: [] };
11
+ if (fs.existsSync(ACCOUNTS_PATH)) {
12
+ const data = fs.readFileSync(ACCOUNTS_PATH, "utf8");
13
+ accounts = JSON.parse(data);
14
+ }
15
+ if (accounts.currentAccount) {
16
+ await keytar.deletePassword("nlx-cli", accounts.currentAccount);
17
+ accounts.accounts = accounts.accounts.filter((acc) => acc !== accounts.currentAccount);
18
+ if (accounts.accounts.length > 0) {
19
+ accounts.currentAccount = accounts.accounts[0];
20
+ await fs.promises.writeFile(ACCOUNTS_PATH, JSON.stringify(accounts));
21
+ }
22
+ else {
23
+ await fs.promises.unlink(ACCOUNTS_PATH);
24
+ }
25
+ }
26
+ consola.success("Logged out. Tokens cleared from storage.");
27
+ }
28
+ catch (err) {
29
+ consola.error(`Logout failed: ${err?.message || err}`);
30
+ process.exitCode = 1;
31
+ }
32
+ });
@@ -0,0 +1,30 @@
1
+ import { Command } from "commander";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { consola } from "consola";
6
+ const ACCOUNTS_PATH = path.join(os.homedir(), ".nlx-cli-auth.json");
7
+ export const authSwitchCommand = new Command("switch")
8
+ .description("Switch to the next saved NLX account")
9
+ .action(async () => {
10
+ if (!fs.existsSync(ACCOUNTS_PATH)) {
11
+ consola.error("No accounts file found. Please login first.");
12
+ process.exit(1);
13
+ }
14
+ const data = fs.readFileSync(ACCOUNTS_PATH, "utf8");
15
+ const accounts = JSON.parse(data);
16
+ if (!accounts.accounts || accounts.accounts.length === 0) {
17
+ consola.error("No saved accounts found.");
18
+ process.exit(1);
19
+ }
20
+ if (!accounts.currentAccount) {
21
+ accounts.currentAccount = accounts.accounts[0];
22
+ }
23
+ else {
24
+ const idx = accounts.accounts.indexOf(accounts.currentAccount);
25
+ const nextIdx = (idx + 1) % accounts.accounts.length;
26
+ accounts.currentAccount = accounts.accounts[nextIdx];
27
+ }
28
+ fs.writeFileSync(ACCOUNTS_PATH, JSON.stringify(accounts, null, 2));
29
+ consola.success(`Switched to account: ${accounts.currentAccount}`);
30
+ });
@@ -0,0 +1,33 @@
1
+ import { Command } from "commander";
2
+ import { consola } from "consola";
3
+ import { fetchManagementApi } from "../../utils/index.js";
4
+ const WHOAMI_DESCRIPTION = "Show current team, role, and available applications count (requires login)";
5
+ function countBots(response) {
6
+ if (!response || !Array.isArray(response.bots))
7
+ return 0;
8
+ return response.bots.length;
9
+ }
10
+ export async function runWhoami() {
11
+ const [team, bots] = await Promise.all([
12
+ fetchManagementApi("team"),
13
+ fetchManagementApi("bots"),
14
+ ]);
15
+ const teamName = team?.name ?? "Unknown";
16
+ const applicationsCount = countBots(bots);
17
+ consola.success("You are authenticated");
18
+ consola.info(`Workspace: ${teamName}`);
19
+ consola.info(`Role: ${team?.currentRole ?? "Unknown"}`);
20
+ consola.info(`Applications: ${applicationsCount}`);
21
+ }
22
+ export const whoamiCommand = new Command("whoami")
23
+ .description(WHOAMI_DESCRIPTION)
24
+ .action(async () => {
25
+ try {
26
+ await runWhoami();
27
+ }
28
+ catch (err) {
29
+ const message = err?.response?.data?.message || err?.message || String(err);
30
+ consola.error(`whoami failed: ${message}`);
31
+ process.exitCode = 1;
32
+ }
33
+ });
@@ -0,0 +1,5 @@
1
+ import { Command } from "commander";
2
+ import { syncCommand } from "./sync.js";
3
+ export const dataRequestsCommand = new Command("data-requests")
4
+ .description("Data Requests")
5
+ .addCommand(syncCommand);
@@ -0,0 +1,328 @@
1
+ import { editor, expand, select } from "@inquirer/prompts";
2
+ import boxen from "boxen";
3
+ import chalk from "chalk";
4
+ import { Command } from "commander";
5
+ import { fetchManagementApi } from "../../utils/index.js";
6
+ import OASNormalize from "oas-normalize";
7
+ import Oas from "oas";
8
+ import open from "open";
9
+ import { consola } from "consola";
10
+ const categorizeServers = async (spec, interactive) => {
11
+ const { servers } = spec.getDefinition();
12
+ if (servers == null)
13
+ throw new Error("No servers defined in the specification");
14
+ if (servers.length === 1)
15
+ return { production: 0 };
16
+ const guess = servers.reduce((acc, server, idx) => {
17
+ if (server.description != null &&
18
+ server.description.match(/prod(uction)?/i))
19
+ acc.production = idx;
20
+ else if (server.description != null &&
21
+ server.description.match(/dev(elopment)?|staging/i))
22
+ acc.development = idx;
23
+ return acc;
24
+ }, {});
25
+ if (interactive) {
26
+ const servers = spec.getDefinition().servers;
27
+ if (servers && servers.length > 1) {
28
+ const serverChoices = servers.map((srv, idx) => ({
29
+ name: `${srv.url}`,
30
+ value: idx,
31
+ description: srv.description,
32
+ }));
33
+ const prodIdx = await select({
34
+ message: "Select production server:",
35
+ choices: serverChoices,
36
+ default: guess.production,
37
+ });
38
+ const devIdx = await select({
39
+ message: "Select development server:",
40
+ choices: serverChoices.concat([
41
+ {
42
+ name: "No development server",
43
+ value: undefined,
44
+ description: undefined,
45
+ },
46
+ ]),
47
+ default: guess.development,
48
+ });
49
+ return { production: prodIdx, development: devIdx };
50
+ }
51
+ }
52
+ return guess;
53
+ };
54
+ const resolveAmbiguity = (arr, interactive, id, label) => {
55
+ if (arr.length === 0)
56
+ return Promise.resolve(undefined);
57
+ if (arr.length === 1)
58
+ return Promise.resolve(arr[0]);
59
+ if (interactive) {
60
+ return select({
61
+ message: `Select ${label} in ${id}:`,
62
+ choices: arr.map((item, index) => ({
63
+ name: JSON.stringify(item, null, 2),
64
+ value: item,
65
+ })),
66
+ });
67
+ }
68
+ return Promise.resolve(arr[0]);
69
+ };
70
+ const capitalize = (str) => {
71
+ return str[0].toUpperCase() + str.slice(1);
72
+ };
73
+ export const syncCommand = new Command("sync")
74
+ .summary("Sync Data Requests from an OpenAPI Specification or Swagger")
75
+ .description(`Takes an OpenAPI Specification or Swagger file and creates or updates data requests in your NLX account to match the API definition.
76
+
77
+ Supports OpenAPI 2.0, 3.0, 3.1, Swagger in JSON and YAML formats. Also experimentally supports Postman collections.
78
+
79
+ NLX Data Requests only support a subset of HTTP functionality described in the OpenAPI Specification.
80
+ Operations that don't meet the following will be skipped:
81
+
82
+ - Request and response bodies must be in JSON format.
83
+ - Endpoints must either be unsecured or use API key or Bearer token authentication.
84
+ - Operations must not be marked deprecated.
85
+
86
+ The following features are not supported and will be silently ignored:
87
+
88
+ - Query parameters.
89
+ - Cookies.
90
+ - Fancier JSON Schema features outside the NLX data model.
91
+ - Multiple response schemas (only the first 2xx response is used).
92
+ `)
93
+ .argument("<input-spec>", "Path to the OpenAPI Specification or Swagger file")
94
+ .requiredOption("--folder <folder>", "Folder where the data requests will be organized")
95
+ .optionsGroup("Security mechanism:")
96
+ .option("--api-key-secret <secret>", "Name of the NLX Secret containing the secret to use for the API key security mechanism")
97
+ .option("--bearer-secret <secret>", "Name of the NLX Secret containing the secret to use for the Bearer token security mechanism")
98
+ .optionsGroup("Run modes:")
99
+ .option("--dry-run", "Simulate the sync process without making any changes")
100
+ .option("--interactive", "Prompt for each operation before syncing, allowing user to approve, skip, or resolve ambiguities")
101
+ .action(async (inputSpec, options) => {
102
+ const currentData = new Map((await fetchManagementApi("variables?size=1000")).variables.map((variable) => [variable.variableId, variable]));
103
+ let oas = new OASNormalize(inputSpec, { enablePaths: true });
104
+ try {
105
+ await oas.validate();
106
+ }
107
+ catch (error) {
108
+ consola.error("Failed to validate OpenAPI Specification:", error);
109
+ process.exit(1);
110
+ }
111
+ const spec = Oas.init((await oas.convert()));
112
+ await spec.dereference();
113
+ const serverIndices = await categorizeServers(spec, options.interactive ?? false);
114
+ const newData = await Promise.all(Object.entries(spec.getPaths()).flatMap(([path, methods]) => {
115
+ return Object.values(methods)
116
+ .filter((operation) => {
117
+ return (operation.getContentType() === "application/json" &&
118
+ !operation.isDeprecated() &&
119
+ !operation.isWebhook());
120
+ })
121
+ .filter((operation) => {
122
+ const keys = Object.keys(operation.prepareSecurity());
123
+ if (keys.length === 0 ||
124
+ keys.includes("Header") ||
125
+ keys.includes("Bearer")) {
126
+ return true;
127
+ }
128
+ else {
129
+ consola.warn(`Skipping operation ${operation.getOperationId()} due to unsupported security schemes: ${keys.join(", ")}`);
130
+ return false;
131
+ }
132
+ })
133
+ .map(async (operation) => {
134
+ const variableId = capitalize(operation
135
+ .getOperationId()
136
+ .replace(/[^a-zA-Z0-9]+([a-zA-Z0-9]|$)/g, (_, v) => {
137
+ return v.toUpperCase();
138
+ }));
139
+ const codes = await resolveAmbiguity(operation
140
+ .getResponseStatusCodes()
141
+ .map(parseInt)
142
+ .filter((r) => r >= 200 && r < 300), options.interactive, variableId, "response status codes");
143
+ operation
144
+ .getParameters()
145
+ .filter((op) => ["cookie", "query"].includes(op.in))
146
+ .forEach((param) => {
147
+ consola.warn(`${param.in} param ${param.name} not supported, skipping.`);
148
+ });
149
+ let headers = operation.getHeaders().request.map((header) => ({
150
+ key: header,
151
+ value: "",
152
+ dynamic: true,
153
+ required: false,
154
+ }));
155
+ const security = operation.prepareSecurity();
156
+ if (security.Header &&
157
+ security.Header.length === 1 &&
158
+ options.apiKeySecret &&
159
+ security.Header[0].type === "apiKey") {
160
+ if (options.apiKeySecret) {
161
+ const apiSecretSecurity = security.Header.filter((h) => h.type === "apiKey");
162
+ if (apiSecretSecurity.length === 1 &&
163
+ apiSecretSecurity[0].type === "apiKey" &&
164
+ apiSecretSecurity[0].in === "header") {
165
+ const name = apiSecretSecurity[0].name;
166
+ headers = [
167
+ ...headers.filter((h) => h.key !== name),
168
+ {
169
+ key: name,
170
+ value: `{${options.apiKeySecret}:NLX.Secret}`,
171
+ dynamic: false,
172
+ required: true,
173
+ },
174
+ ];
175
+ }
176
+ }
177
+ }
178
+ if (options.bearerSecret) {
179
+ const bearerSecurity = security.Bearer?.filter((h) => h.type === "http" && h.scheme === "bearer") ?? [];
180
+ if (bearerSecurity.length === 1) {
181
+ headers = [
182
+ ...headers.filter((h) => h.key !== "Authorization"),
183
+ {
184
+ key: "Authorization",
185
+ value: `Bearer {${options.bearerSecret}:NLX.Secret}`,
186
+ dynamic: false,
187
+ required: true,
188
+ },
189
+ ];
190
+ }
191
+ }
192
+ return {
193
+ variableId,
194
+ path: options.folder,
195
+ type: "text",
196
+ description: operation.getSummary() || operation.getDescription(),
197
+ requestSchema: await extractSchema(operation.getRequestBody("application/json") || undefined, options.interactive, variableId, "request schema"),
198
+ responseSchema: await extractSchema(codes ? operation.getResponseAsJSONSchema(codes) : undefined, options.interactive, variableId, "response schema"),
199
+ webhook: {
200
+ implementation: "external",
201
+ method: operation.method.toUpperCase(),
202
+ sendContext: false,
203
+ version: "v3",
204
+ environments: {
205
+ production: {
206
+ url: spec.url(serverIndices.production) + operation.path,
207
+ headers,
208
+ },
209
+ development: serverIndices.development == null
210
+ ? undefined
211
+ : {
212
+ url: spec.url(serverIndices.development) +
213
+ operation.path,
214
+ headers,
215
+ },
216
+ },
217
+ },
218
+ };
219
+ });
220
+ }));
221
+ for (const dataRequest of newData) {
222
+ let proceed = true;
223
+ let resolvedRequest = { ...dataRequest };
224
+ let action;
225
+ if (options.interactive) {
226
+ const preview = [
227
+ chalk.bold(dataRequest.webhook.method +
228
+ " " +
229
+ chalk.underline(dataRequest.webhook.environments.production.url)),
230
+ chalk.dim(dataRequest.description),
231
+ "",
232
+ chalk.bold.magenta(`Request Schema:`),
233
+ chalk.white(JSON.stringify(dataRequest.requestSchema, null, 2)),
234
+ "",
235
+ chalk.bold.magenta(`Response Schema:`),
236
+ chalk.white(JSON.stringify(dataRequest.responseSchema, null, 2)),
237
+ ].join("\n");
238
+ consola.log(boxen(preview, {
239
+ padding: 1,
240
+ margin: 1,
241
+ borderStyle: "round",
242
+ borderColor: "cyan",
243
+ backgroundColor: "black",
244
+ title: dataRequest.variableId,
245
+ }));
246
+ action = await expand({
247
+ message: "Sync this operation?",
248
+ choices: [
249
+ { key: "y", name: "Sync", value: "sync" },
250
+ { key: "n", name: "Skip", value: "skip" },
251
+ { key: "e", name: "Edit in your $EDITOR", value: "edit" },
252
+ { key: "u", name: "Sync then edit in UI", value: "edit-ui" },
253
+ ],
254
+ default: "y",
255
+ });
256
+ if (action === "skip") {
257
+ proceed = false;
258
+ }
259
+ else if (action === "edit") {
260
+ const res = await editor({
261
+ message: "Open JSON in your editor",
262
+ default: JSON.stringify(dataRequest, null, 2),
263
+ postfix: ".json",
264
+ waitForUseInput: false,
265
+ validate: (text) => {
266
+ try {
267
+ JSON.parse(text);
268
+ return true;
269
+ }
270
+ catch {
271
+ return "Invalid JSON";
272
+ }
273
+ },
274
+ });
275
+ resolvedRequest = JSON.parse(res);
276
+ }
277
+ }
278
+ if (!proceed)
279
+ continue;
280
+ const existing = currentData.get(resolvedRequest.variableId);
281
+ if (existing) {
282
+ if (!Object.entries(resolvedRequest).every(([key, value]) => {
283
+ return eq(value, existing[key]);
284
+ })) {
285
+ await wrapDataReqCU(false, resolvedRequest, options.dryRun, action);
286
+ }
287
+ }
288
+ else {
289
+ await wrapDataReqCU(true, resolvedRequest, options.dryRun, action);
290
+ }
291
+ }
292
+ });
293
+ const wrapDataReqCU = async (create, dataRequest, dryRun, action) => {
294
+ if (dryRun) {
295
+ consola.info(`Would ${create ? "create new" : "update existing"} data request ${dataRequest.variableId} ${dataRequest.webhook.method} ${dataRequest.webhook.environments.production.url}, but skipping because --dry-run.`);
296
+ }
297
+ else {
298
+ consola.start(`${create ? "Creating new" : "Updating existing"} data request ${dataRequest.variableId} ${dataRequest.webhook.method} ${dataRequest.webhook.environments.production.url}...`);
299
+ await fetchManagementApi(`variables/${dataRequest.variableId}`, create ? "PUT" : "POST", dataRequest);
300
+ consola.success(`Successfully ${create ? "Created new" : "Updated existing"} data request ${dataRequest.variableId} ${dataRequest.webhook.method} ${dataRequest.webhook.environments.production.url}.`);
301
+ if (action === "edit-ui") {
302
+ open(`https://dev.platform.nlx.ai/data-requests/${dataRequest.variableId}`);
303
+ }
304
+ }
305
+ };
306
+ const extractSchema = async (schema, interactive, id, label) => {
307
+ if (schema == null)
308
+ return undefined;
309
+ if (Array.isArray(schema) && schema.length > 0) {
310
+ schema = await resolveAmbiguity(schema, interactive, id, label);
311
+ }
312
+ if (schema != null && "schema" in schema) {
313
+ schema = schema.schema;
314
+ }
315
+ return schema;
316
+ };
317
+ const eq = (a, b) => {
318
+ if (a === b)
319
+ return true;
320
+ if (a == null && b == null)
321
+ return true;
322
+ if (typeof a == "object" && typeof b == "object") {
323
+ const aKeys = Object.keys(a);
324
+ const bKeys = Object.keys(b);
325
+ return aKeys.every((key) => eq(a[key], b[key]));
326
+ }
327
+ return false;
328
+ };
@@ -0,0 +1,60 @@
1
+ import { Command } from "commander";
2
+ import * as fs from "fs";
3
+ import { fetchManagementApi } from "../utils/index.js";
4
+ import { consola } from "consola";
5
+ export const httpCommand = new Command("http")
6
+ .description("Perform an authenticated request to the management API")
7
+ .argument("<method>", "HTTP method (GET, POST, PUT, DELETE, etc.)")
8
+ .argument("<path>", "API path (e.g. /models)")
9
+ .argument("[body]", "Request body as JSON string, name of a JSON file or -- for Standard Input")
10
+ .option("-p, --paginate", "Enable pagination", false)
11
+ .action(async (method, apiPath, body, opts) => {
12
+ if (body === "--") {
13
+ // Read from stdin
14
+ body = "";
15
+ process.stdin.setEncoding("utf8");
16
+ for await (const chunk of process.stdin)
17
+ body += chunk;
18
+ try {
19
+ body = JSON.parse(body);
20
+ }
21
+ catch (ed) {
22
+ consola.error("Invalid JSON string from stdin: " + ed);
23
+ process.exit(1);
24
+ }
25
+ }
26
+ else if (body != null) {
27
+ // Try to parse as file path or JSON string
28
+ try {
29
+ if (fs.existsSync(body)) {
30
+ body = JSON.parse(fs.readFileSync(body, "utf8"));
31
+ }
32
+ else {
33
+ body = JSON.parse(body);
34
+ }
35
+ }
36
+ catch {
37
+ consola.error("Invalid JSON string or file path: " + body);
38
+ process.exit(1);
39
+ }
40
+ }
41
+ let result = await fetchManagementApi(apiPath +
42
+ (opts.paginate
43
+ ? apiPath.includes("?")
44
+ ? "&size=1000"
45
+ : "?size=1000"
46
+ : ""), method.toUpperCase(), body);
47
+ let agg;
48
+ if (opts.paginate) {
49
+ const key = Object.keys(result).filter((k) => k !== "nextPageId")[0];
50
+ agg = result[key];
51
+ while (result.nextPageId) {
52
+ result = await fetchManagementApi(apiPath + `?pageId=${result.nextPageId}`, method.toUpperCase(), body);
53
+ agg.push(...result[key]);
54
+ }
55
+ }
56
+ else {
57
+ agg = result;
58
+ }
59
+ console.log(JSON.stringify(agg, null, 2));
60
+ });
@@ -0,0 +1,99 @@
1
+ import { Command } from "commander";
2
+ import { promises as fs, existsSync } from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { fetchManagementApi } from "../../utils/index.js";
6
+ import ts from "typescript";
7
+ import { compile } from "json-schema-to-typescript";
8
+ import chalk from "chalk";
9
+ import { consola } from "consola";
10
+ export const modalitiesCheckCommand = new Command("check")
11
+ .description("Type check local TypeScript definitions against server schemas")
12
+ .argument("[file]", "TypeScript file to check (e.g. one generated by 'modalities generate')", "modalities-types.d.ts")
13
+ .action(async (file) => {
14
+ // Fetch server models
15
+ const data = await fetchManagementApi(`models`);
16
+ const serverModels = {};
17
+ for (const item of data.items) {
18
+ serverModels[item.modelId] = item.schema;
19
+ }
20
+ // Read local TypeScript file
21
+ const filePath = path.resolve(process.cwd(), file);
22
+ if (!existsSync(filePath)) {
23
+ consola.error(`File not found: ${filePath}`);
24
+ process.exit(1);
25
+ }
26
+ const tsSource = await fs.readFile(filePath, "utf8");
27
+ // Generate remote types from schemas
28
+ let remoteTypesSource = "";
29
+ for (const modelId in serverModels) {
30
+ const typeName = modelId.replace(/[^a-zA-Z0-9_]/g, "");
31
+ const tsType = await compile(serverModels[modelId], typeName, {
32
+ bannerComment: "",
33
+ additionalProperties: false,
34
+ });
35
+ remoteTypesSource += tsType + "\n";
36
+ }
37
+ // Create a virtual TypeScript program with both local and remote types
38
+ const tmpRemotePath = path.join(os.tmpdir(), `modalities-remote-${Date.now()}.d.ts`);
39
+ await fs.writeFile(tmpRemotePath, remoteTypesSource);
40
+ const program = ts.createProgram([filePath, tmpRemotePath], {});
41
+ const checker = program.getTypeChecker();
42
+ // Get local and remote type symbols
43
+ const localSourceFile = program.getSourceFile(filePath);
44
+ const remoteSourceFile = program.getSourceFile(tmpRemotePath);
45
+ if (!localSourceFile || !remoteSourceFile) {
46
+ consola.error("Could not load source files for type checking.");
47
+ process.exit(1);
48
+ }
49
+ // Map type names to symbols
50
+ const getTypeSymbols = (sourceFile) => {
51
+ const symbols = {};
52
+ sourceFile.forEachChild((node) => {
53
+ if (ts.isInterfaceDeclaration(node) ||
54
+ ts.isTypeAliasDeclaration(node)) {
55
+ const name = node.name.text;
56
+ const symbol = checker.getSymbolAtLocation(node.name);
57
+ if (symbol)
58
+ symbols[name] = symbol;
59
+ }
60
+ });
61
+ return symbols;
62
+ };
63
+ const localSymbols = getTypeSymbols(localSourceFile);
64
+ const remoteSymbols = getTypeSymbols(remoteSourceFile);
65
+ // Check assignability
66
+ let errors = [];
67
+ for (const typeName in localSymbols) {
68
+ if (!remoteSymbols[typeName]) {
69
+ errors.push(`Type/interface '${typeName}' does not correspond to any model on the server.`);
70
+ continue;
71
+ }
72
+ const localType = checker.getDeclaredTypeOfSymbol(localSymbols[typeName]);
73
+ const remoteType = checker.getDeclaredTypeOfSymbol(remoteSymbols[typeName]);
74
+ if (!checker.isTypeAssignableTo(remoteType, localType)) {
75
+ errors.push(`NLX modality type for '${typeName}' is not assignable to local type.
76
+
77
+ ${chalk.bold("NLX modality type definition:")}
78
+
79
+ ${remoteSymbols[typeName].declarations?.[0]?.getText() ?? checker.typeToString(remoteType)}
80
+
81
+ ${chalk.bold("Local type definition:")}
82
+
83
+ ${localSymbols[typeName].declarations?.[0]?.getText() ?? checker.typeToString(localType)}
84
+ `);
85
+ }
86
+ }
87
+ await fs.unlink(tmpRemotePath);
88
+ if (errors.length) {
89
+ let errorMsg = "Type check failed:\n";
90
+ for (const err of errors)
91
+ errorMsg += ` - ${err}\n`;
92
+ consola.error(errorMsg.trim());
93
+ process.exit(1);
94
+ }
95
+ else {
96
+ consola.success("Type check passed: all remote types are assignable to local types.");
97
+ process.exit(0);
98
+ }
99
+ });
@@ -0,0 +1,27 @@
1
+ import { Command } from "commander";
2
+ import { compile } from "json-schema-to-typescript";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import { fetchManagementApi } from "../../utils/index.js";
6
+ import { consola } from "consola";
7
+ export const modalitiesGenerateCommand = new Command("generate")
8
+ .description("Fetch modalities and generate TypeScript interfaces")
9
+ .option("-o, --out <file>", "Output TypeScript file", "modalities-types.d.ts")
10
+ .action(async (opts) => {
11
+ const data = await fetchManagementApi(`models`);
12
+ // Generate TypeScript interfaces for each modelId
13
+ let output = "// Auto-generated from NLX\n// Please do not edit manually\n\n";
14
+ for (const item of data.items) {
15
+ const name = item.modelId.replace(/[^a-zA-Z0-9_]/g, "");
16
+ const schema = item.schema;
17
+ const ts = await compile(schema, name, {
18
+ bannerComment: "",
19
+ additionalProperties: false,
20
+ });
21
+ output += ts + "\n";
22
+ }
23
+ // Write to file specified by flag or default
24
+ const outPath = path.resolve(process.cwd(), opts.out);
25
+ fs.writeFileSync(outPath, output);
26
+ consola.success(`TypeScript interfaces written to ${outPath}`);
27
+ });
@@ -0,0 +1,7 @@
1
+ import { Command } from "commander";
2
+ import { modalitiesGenerateCommand } from "./generate.js";
3
+ import { modalitiesCheckCommand } from "./check.js";
4
+ export const modalitiesCommand = new Command("modalities")
5
+ .description("Work with NLX modalities")
6
+ .addCommand(modalitiesGenerateCommand)
7
+ .addCommand(modalitiesCheckCommand);
@@ -0,0 +1,208 @@
1
+ import { Command } from "commander";
2
+ import { fetchManagementApi } from "../utils/index.js";
3
+ import { consola } from "consola";
4
+ import { createConversation, } from "@nlxai/core";
5
+ import { ensureToken } from "./auth/login.js";
6
+ import { flatten, uniq } from "ramda";
7
+ import chalk from "chalk";
8
+ import boxen from "boxen";
9
+ export const testCommand = new Command("test")
10
+ .description("Run conversation tests for a given application ID")
11
+ .argument("<applicationId>", "Application ID to fetch tests for")
12
+ .option("--env <environment>", "Specify the environment", "production")
13
+ .option("--language <language>", "Specify the language code", "en-US")
14
+ .option("--channel <channel>", "Specify the channel type", "API")
15
+ .option("--applications-url-base-override <url>", "Override the base URL for applications")
16
+ .option("--enterprise-region <region>", "Specify the enterprise region")
17
+ .action(async (applicationId, opts) => {
18
+ try {
19
+ const { tests } = (await fetchManagementApi(`bots/${applicationId}/conversationTests`, "GET"));
20
+ consola.log("Fetched %i tests. Running...", tests.length);
21
+ const baseUrl = getBaseUrl(opts.enterpriseRegion == null, opts.applicationsUrlBaseOverride ?? "", opts.enterpriseRegion ?? "US");
22
+ const applicationUrl = `${baseUrl}/c/${applicationId}/sandbox`;
23
+ consola.debug("Application URL:", applicationUrl);
24
+ const accessToken = await ensureToken();
25
+ const handlerConfig = {
26
+ applicationUrl,
27
+ headers: {
28
+ "nlx-api-key": accessToken,
29
+ Authorization: `Bearer ${accessToken}`,
30
+ },
31
+ environment: opts.env,
32
+ languageCode: opts.language,
33
+ experimental: {
34
+ channelType: opts.channel,
35
+ completeApplicationUrl: true,
36
+ },
37
+ };
38
+ const failures = [];
39
+ for await (const test of tests) {
40
+ const result = await runTest(test, handlerConfig, applicationId);
41
+ if (result.met.length === result.total.length) {
42
+ consola.success(test.name);
43
+ }
44
+ else {
45
+ failures.push({
46
+ name: test.name,
47
+ met: result.met.length,
48
+ total: result.total.length,
49
+ responses: result.responses,
50
+ intentId: test.steps[0]?.raw?.intentId,
51
+ });
52
+ consola.error(`${test.name} failed: ${result.met.length}/${result.total.length}`);
53
+ }
54
+ }
55
+ if (failures.length > 0) {
56
+ consola.log("--------------------------------");
57
+ consola.log("%i tests failed", failures.length);
58
+ failures.forEach((failure) => {
59
+ consola.error("Test: %s", failure.name);
60
+ consola.log("Assertions met: %i/%i", failure.met, failure.total);
61
+ console.log("Transcript:");
62
+ failure.responses.forEach((response, index) => {
63
+ if (response.type === "user") {
64
+ consola.log(` %i. ${chalk.blue("User")}: %s`, index + 1, response.payload.type === "structured"
65
+ ? `${chalk.underline(response.payload.intentId)}`
66
+ : response.payload.type == "choice"
67
+ ? `➡️ ${response.payload.choiceId}`
68
+ : response.payload.text);
69
+ }
70
+ else if (response.type === "bot") {
71
+ consola.log(` %i. ${chalk.yellow("Bot")}:`, index + 1);
72
+ consola.log(response.payload.messages
73
+ .map((msg) => boxen(msg.text, { margin: { left: 4, bottom: 1 } }))
74
+ .join("\n"));
75
+ }
76
+ });
77
+ consola.log("Debug at: ", chalk.underline(`https://dev.platform.nlx.ai/flows/${failure.intentId}`));
78
+ });
79
+ process.exit(1);
80
+ }
81
+ else {
82
+ consola.success("All tests passed!");
83
+ }
84
+ }
85
+ catch (err) {
86
+ consola.error("Failed to fetch tests:", err);
87
+ process.exit(1);
88
+ }
89
+ });
90
+ const runTest = async (test, config, applicationId) => {
91
+ const handler = createConversation(config);
92
+ const responses = await replayConversation({
93
+ steps: test.steps,
94
+ conversationHandler: handler,
95
+ });
96
+ const { total, met } = assertionsSummary(responses, test.assertions);
97
+ handler.destroy();
98
+ await fetchReset(applicationId);
99
+ return { total, met, responses };
100
+ };
101
+ const fetchReset = async (applicationId) => {
102
+ return await fetchManagementApi(`bots/${applicationId}/sandbox/reset`, "POST");
103
+ };
104
+ const replayConversation = async ({ steps, conversationHandler, }) => {
105
+ if (steps.length === 0) {
106
+ return [];
107
+ }
108
+ const [firstStep, ...restSteps] = steps;
109
+ if (firstStep.raw.type === "structured" && (firstStep.raw.poll ?? false)) {
110
+ return replayConversation({
111
+ steps: restSteps,
112
+ conversationHandler,
113
+ });
114
+ }
115
+ return await new Promise((resolve) => {
116
+ const handleResponse = (responses, newResponse) => {
117
+ if (newResponse == null) {
118
+ return;
119
+ }
120
+ if (newResponse.type === "bot" &&
121
+ !(newResponse.payload.metadata?.hasPendingDataRequest ?? false)) {
122
+ setTimeout(() => {
123
+ conversationHandler.unsubscribe(handleResponse);
124
+ if (restSteps.length === 0) {
125
+ resolve(responses);
126
+ }
127
+ else {
128
+ resolve(replayConversation({
129
+ steps: restSteps,
130
+ conversationHandler,
131
+ }));
132
+ }
133
+ }, 250);
134
+ }
135
+ else if (isTestCompleted(responses, steps)) {
136
+ resolve(responses);
137
+ }
138
+ };
139
+ conversationHandler.subscribe(handleResponse);
140
+ if (firstStep.raw.type === "text") {
141
+ conversationHandler.sendText(firstStep.raw.text, firstStep.raw.context);
142
+ }
143
+ else if (firstStep.raw.type === "choice") {
144
+ // TODO: add response and message indices to fix button click states
145
+ conversationHandler.sendChoice(firstStep.raw.choiceId, firstStep.raw.context);
146
+ }
147
+ else if (firstStep.raw.type === "structured") {
148
+ conversationHandler.sendStructured({
149
+ // eslint-disable-next-line deprecation/deprecation
150
+ intentId: firstStep.raw.intentId,
151
+ choiceId: firstStep.raw.choiceId,
152
+ slots: firstStep.raw.slots,
153
+ }, firstStep.raw.context);
154
+ }
155
+ });
156
+ };
157
+ const isTestCompleted = (responses, steps) => {
158
+ let pendingTestStepIndex = 0;
159
+ let index = 0;
160
+ for (const response of responses) {
161
+ if (response.type === "user") {
162
+ pendingTestStepIndex++;
163
+ }
164
+ // If all test steps have been triggered and there is a subsequent application response available, the test is considered complete
165
+ if (pendingTestStepIndex === steps.length + 1 && response.type === "bot") {
166
+ return true;
167
+ }
168
+ index++;
169
+ }
170
+ return false;
171
+ };
172
+ export const assertionsSummary = (responses, assertions) => {
173
+ const assertionNodes = getAssertionNodes(assertions);
174
+ const applicationResponses = responses.filter((res) => res.type === "bot");
175
+ const payloads = applicationResponses.map((res) => res.payload);
176
+ const nodesHit = applicationResponseTraversedNodeIds(payloads);
177
+ return {
178
+ met: nodesHit.filter((nodeId) => assertionNodes.includes(nodeId)),
179
+ total: assertionNodes,
180
+ };
181
+ };
182
+ const applicationResponseTraversedNodeIds = (application) => uniq(flatten(application.map((payload) => traversalNodeIds(payload.debugEvents ?? []))));
183
+ const traversalNodeIds = (events) => {
184
+ const nodeIds = [];
185
+ events.forEach((event) => {
186
+ if (event.eventType === "NodeTraversal" &&
187
+ typeof event.nodeId === "string") {
188
+ nodeIds.push(event.nodeId);
189
+ }
190
+ });
191
+ return nodeIds;
192
+ };
193
+ const getAssertionNodes = (assertions) => {
194
+ return uniq(assertions
195
+ .map(({ nodeId }) => nodeId)
196
+ .filter((nodeId) => nodeId != null));
197
+ };
198
+ const getBaseUrl = (isGa, applicationsUrlBaseOverride, region) => {
199
+ const httpsBaseUrl = isGa
200
+ ? `https://dev.apps.nlx.ai`
201
+ : `https://bots.dev.studio.nlx.ai`;
202
+ const baseUrl = applicationsUrlBaseOverride.length > 0
203
+ ? applicationsUrlBaseOverride
204
+ : region === "EU"
205
+ ? httpsBaseUrl.replace("//", "//eu-central-1.")
206
+ : httpsBaseUrl;
207
+ return baseUrl;
208
+ };
package/lib/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import { program } from "commander";
2
+ import { authCommand } from "./commands/auth/index.js";
3
+ import { modalitiesCommand } from "./commands/modalities/index.js";
4
+ import { dataRequestsCommand } from "./commands/data-requests/index.js";
5
+ import { httpCommand } from "./commands/http.js";
6
+ import { testCommand } from "./commands/test.js";
7
+ program.description("Intereact with NLX from the command line");
8
+ program.addCommand(authCommand);
9
+ program.addCommand(modalitiesCommand);
10
+ program.addCommand(dataRequestsCommand);
11
+ program.addCommand(testCommand);
12
+ program.addCommand(httpCommand);
13
+ program.parse(process.argv);
@@ -0,0 +1,19 @@
1
+ import { ensureToken } from "../commands/auth/login.js";
2
+ import { consola } from "consola";
3
+ const API_BASE_URL = process.env.NLX_API_BASE_URL || "https://api.dev.studio.nlx.ai/v1";
4
+ export const fetchManagementApi = async (path, method = "GET", body) => {
5
+ const accessToken = await ensureToken();
6
+ const res = await fetch(`${API_BASE_URL}/${path}`, {
7
+ headers: {
8
+ Authorization: `Bearer ${accessToken}`,
9
+ Accept: "application/json",
10
+ },
11
+ method,
12
+ body: body ? JSON.stringify(body) : undefined,
13
+ });
14
+ if (!res.ok) {
15
+ consola.error("Failed to fetch:", res.status, await res.text());
16
+ process.exit(1);
17
+ }
18
+ return await res.json();
19
+ };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@nlxai/cli",
3
+ "version": "1.2.2-alpha.2",
4
+ "description": "Tools for integrating with NLX apps",
5
+ "keywords": [
6
+ "NLX",
7
+ "AI",
8
+ "typescript"
9
+ ],
10
+ "type": "module",
11
+ "author": "Jakub Hampl <jakub@nlx.ai>",
12
+ "homepage": "https://github.com/nlxai/sdk#readme",
13
+ "license": "MIT",
14
+ "main": "lib/index.ts",
15
+ "bin": {
16
+ "npx": "bin/npx"
17
+ },
18
+ "directories": {
19
+ "lib": "lib"
20
+ },
21
+ "files": [
22
+ "bin",
23
+ "lib"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/nlxai/sdk.git"
28
+ },
29
+ "scripts": {
30
+ "dev": "vitest",
31
+ "test": "vitest run",
32
+ "build": "tsc",
33
+ "tsc": "tsc"
34
+ },
35
+ "dependencies": {
36
+ "@inquirer/prompts": "^7.8.4",
37
+ "@nlxai/core": "^1.1.8-alpha.0",
38
+ "boxen": "^8.0.1",
39
+ "chalk": "^4.1.0",
40
+ "commander": "^14.0",
41
+ "consola": "^3.4.2",
42
+ "json-schema-to-typescript": "^15.0.4",
43
+ "keytar": "^7.9.0",
44
+ "oas": "^28.1.0",
45
+ "oas-normalize": "^15.0.1",
46
+ "open": "^8.4.2",
47
+ "typescript": "^5.4.5"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/nlxai/sdk/issues"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "devDependencies": {
56
+ "@vitest/coverage-istanbul": "^3.2.4",
57
+ "@vitest/coverage-v8": "^3.2.4",
58
+ "@vitest/ui": "^3.2.4",
59
+ "vitest": "^3.2.4"
60
+ },
61
+ "gitHead": "326f4ca5d7362bf8f3c01acfd9f01844fdf53904"
62
+ }