@velaro/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # Velaro CLI
2
+
3
+ Command-line interface for managing your Velaro account — bots, knowledge base ingestion, and MCP API keys.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @velaro/cli
9
+ ```
10
+
11
+ Or run without installing:
12
+
13
+ ```bash
14
+ npx @velaro/cli login
15
+ ```
16
+
17
+ > Requires Node.js 18 or later.
18
+
19
+ ## Authentication
20
+
21
+ Velaro CLI uses your existing Velaro account — the same login you use in the admin portal. No separate API key or password required.
22
+
23
+ ```bash
24
+ velaro login
25
+ ```
26
+
27
+ This opens your browser to a short code page. Enter the code shown in the terminal, sign in with your Velaro account, and you're done. Your session is saved to `~/.velaro/config.json` and refreshes automatically — you won't need to log in again for months.
28
+
29
+ ```bash
30
+ velaro logout # clear saved credentials
31
+ velaro whoami # confirm who you're logged in as
32
+ ```
33
+
34
+ ## Commands
35
+
36
+ ### `velaro bot list`
37
+ List all AI bots configured on your site.
38
+
39
+ ```
40
+ [42] Support Bot model=velaro-gpt-4o-mini status=ready
41
+ [43] Sales Bot model=velaro-gpt-4o-mini status=ready
42
+ ```
43
+
44
+ ### `velaro bot setup`
45
+ Create or update an AI bot (upsert by name).
46
+
47
+ ```bash
48
+ velaro bot setup \
49
+ --name "Support Bot" \
50
+ --prompt "You are a helpful support assistant for Acme Corp..." \
51
+ --tone Professional \
52
+ --reply-length Medium \
53
+ --urls "https://help.acme.com,https://acme.com/faq"
54
+ ```
55
+
56
+ Options:
57
+ | Flag | Description | Default |
58
+ |---|---|---|
59
+ | `--name` | Bot name (required) | — |
60
+ | `--prompt` | System prompt | — |
61
+ | `--model` | AI model | `velaro-gpt-4o-mini` |
62
+ | `--tone` | `Professional`, `Friendly`, `Formal`, `Casual` | `Professional` |
63
+ | `--reply-length` | `Short`, `Medium`, `Long` | `Medium` |
64
+ | `--urls` | Comma-separated knowledge base URLs | — |
65
+
66
+ ### `velaro ingest --job-id <id>`
67
+ Trigger knowledge base ingestion for a completed scraper job. The job runs in the background — embedding 50+ pages typically takes 1–2 minutes.
68
+
69
+ ```bash
70
+ velaro ingest --job-id d2d93504
71
+ ```
72
+
73
+ > Requires the **Knowledge Base** feature on your subscription.
74
+
75
+ ### `velaro mcp-key list`
76
+ List MCP API keys for your site (used to connect AI tools like Claude).
77
+
78
+ ```
79
+ [1] Claude Code prefix=vel_live_aB4 active last used=4/9/2026
80
+ [2] Zapier prefix=vel_live_xC7 active last used=never
81
+ ```
82
+
83
+ ### `velaro mcp-key create --label <label>`
84
+ Create a new MCP API key. The full key is shown **once** — copy it immediately.
85
+
86
+ ```bash
87
+ velaro mcp-key create --label "Claude Code"
88
+ velaro mcp-key create --label "Zapier" --expires 2027-01-01
89
+ ```
90
+
91
+ ### `velaro mcp-key revoke <id>`
92
+ Revoke an MCP key by ID.
93
+
94
+ ```bash
95
+ velaro mcp-key revoke 2
96
+ ```
97
+
98
+ ### `velaro status`
99
+ Check that the Velaro API is reachable.
100
+
101
+ ```bash
102
+ velaro status
103
+ # Checking https://velaro-messaging-api-staging.azurewebsites.net/Status ... OK
104
+ ```
105
+
106
+ ## Environment variables
107
+
108
+ | Variable | Description |
109
+ |---|---|
110
+ | `VELARO_API_BASE` | Override the API base URL (defaults to production) |
111
+ | `VELARO_CLI_CLIENT_ID` | Override the Entra app ID (advanced) |
112
+
113
+ ## What the CLI can access
114
+
115
+ The CLI operates under your Velaro account with the same permissions you have in the admin portal. It can only access **your own site's data** — it is not possible to read or modify another customer's account.
116
+
117
+ Feature access is gated by your subscription plan:
118
+ - **Bot setup** — requires the AI feature on your plan
119
+ - **KB ingestion** — requires the Knowledge Base feature
120
+ - **MCP keys** — requires the MCP API Access feature
121
+
122
+ ## Security
123
+
124
+ - Login uses OAuth 2.0 device authorization (RFC 8628) — no password is ever stored
125
+ - Credentials are saved to `~/.velaro/config.json` with owner-only permissions (`0600`)
126
+ - Your Velaro JWT expires after 1 hour and is silently refreshed using a secure refresh token
127
+ - All API calls go over HTTPS to `velaro-messaging-api.azurewebsites.net`
128
+
129
+ ## Publishing (internal)
130
+
131
+ The CLI lives in `velaro-admin/cli/`. To publish to npm:
132
+
133
+ ```bash
134
+ cd cli
135
+ npm publish --access public
136
+ ```
137
+
138
+ Requires `npm login` with the `@velaro` organization access.
package/bin/velaro.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ import yargs from 'yargs';
4
+ import { hideBin } from 'yargs/helpers';
5
+ import { loginCommand, logoutCommand } from '../lib/commands/login.js';
6
+ import { whoamiCommand } from '../lib/commands/whoami.js';
7
+ import { botCommand } from '../lib/commands/bot.js';
8
+ import { ingestCommand } from '../lib/commands/ingest.js';
9
+ import { mcpKeyCommand } from '../lib/commands/mcp-key.js';
10
+ import { statusCommand } from '../lib/commands/status.js';
11
+
12
+ yargs(hideBin(process.argv))
13
+ .scriptName('velaro')
14
+ .usage('$0 <command> [options]')
15
+ .command(loginCommand)
16
+ .command(logoutCommand)
17
+ .command(whoamiCommand)
18
+ .command(botCommand)
19
+ .command(ingestCommand)
20
+ .command(mcpKeyCommand)
21
+ .command(statusCommand)
22
+ .demandCommand(1, 'Specify a command. Run velaro --help for a list.')
23
+ .strict()
24
+ .help()
25
+ .alias('h', 'help')
26
+ .alias('v', 'version')
27
+ .wrap(Math.min(100, process.stdout.columns || 100))
28
+ .argv;
package/lib/api.js ADDED
@@ -0,0 +1,48 @@
1
+ import { readConfig, writeConfig } from './config.js';
2
+ import { refreshVelaroToken } from './oauth.js';
3
+
4
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000;
5
+
6
+ export async function getCredentials() {
7
+ let creds = readConfig();
8
+
9
+ if (!creds?.velaroToken) {
10
+ throw new Error('Not logged in. Run: velaro login');
11
+ }
12
+
13
+ if (Date.now() + REFRESH_BUFFER_MS >= new Date(creds.velaroExpires).getTime()) {
14
+ if (!creds.entraRefreshToken) {
15
+ throw new Error('Session expired. Run: velaro login');
16
+ }
17
+ creds = await refreshVelaroToken(creds);
18
+ writeConfig(creds);
19
+ }
20
+
21
+ return creds;
22
+ }
23
+
24
+ export async function request(method, path, body) {
25
+ const creds = await getCredentials();
26
+
27
+ const res = await fetch(`${creds.apiBase}${path}`, {
28
+ method,
29
+ headers: {
30
+ Authorization: `Bearer ${creds.velaroToken}`,
31
+ 'Content-Type': 'application/json',
32
+ },
33
+ body: body !== undefined ? JSON.stringify(body) : undefined,
34
+ });
35
+
36
+ if (!res.ok) {
37
+ let msg = `${method} ${path} → ${res.status}`;
38
+ try { const t = await res.text(); if (t) msg += `: ${t}`; } catch { /* body unreadable — status already captured */ }
39
+ throw new Error(msg);
40
+ }
41
+
42
+ const text = await res.text();
43
+ return text ? JSON.parse(text) : null;
44
+ }
45
+
46
+ export const get = (path) => request('GET', path);
47
+ export const post = (path, body) => request('POST', path, body);
48
+ export const del = (path) => request('DELETE', path);
@@ -0,0 +1,54 @@
1
+ import { get, post } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ export const botCommand = {
5
+ command: 'bot <subcommand>',
6
+ describe: 'Manage AI bots',
7
+ builder: (yargs) =>
8
+ yargs
9
+ .command(botListCommand)
10
+ .command(botSetupCommand)
11
+ .demandCommand(1, 'Specify a subcommand: list, setup'),
12
+ handler: () => {},
13
+ };
14
+
15
+ const botListCommand = {
16
+ command: 'list',
17
+ describe: 'List AI bots on your site',
18
+ handler: runCommand(async () => {
19
+ const bots = await get('/AIConfiguration/list');
20
+ if (!bots?.length) { console.log('No bots found.'); return; }
21
+ console.log(`Found ${bots.length} bot(s):\n`);
22
+ for (const b of bots) {
23
+ console.log(` [${b.id}] ${b.name} model=${b.aiModel ?? '(default)'} status=${b.status ?? 'ready'}`);
24
+ }
25
+ }),
26
+ };
27
+
28
+ const botSetupCommand = {
29
+ command: 'setup',
30
+ describe: 'Create or update an AI bot (upsert by name)',
31
+ builder: (y) =>
32
+ y
33
+ .option('name', { describe: 'Bot name', type: 'string', demandOption: true })
34
+ .option('prompt', { describe: 'System prompt text', type: 'string' })
35
+ .option('model', { describe: 'AI model override', type: 'string', default: 'velaro-gpt-4o-mini' })
36
+ .option('tone', { describe: 'Response tone', choices: ['Professional','Friendly','Formal','Casual'], default: 'Professional' })
37
+ .option('reply-length', { describe: 'Response length', choices: ['Short','Medium','Long'], default: 'Medium' })
38
+ .option('urls', { describe: 'Comma-separated data source URLs', type: 'string' }),
39
+
40
+ handler: runCommand(async (argv) => {
41
+ const payload = {
42
+ name: argv.name,
43
+ aiModel: argv.model,
44
+ tone: argv.tone,
45
+ replyLength: argv['reply-length'],
46
+ };
47
+
48
+ if (argv.prompt) payload.prompt = argv.prompt;
49
+ if (argv.urls) payload.dataUrls = argv.urls.split(',').map((u) => u.trim()).filter(Boolean);
50
+
51
+ const result = await post('/AIConfiguration', payload);
52
+ console.log(`Saved: [${result.id}] ${result.name}`);
53
+ }),
54
+ };
@@ -0,0 +1,31 @@
1
+ import { getCredentials, post } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ export const ingestCommand = {
5
+ command: 'ingest',
6
+ describe: 'Trigger knowledge base ingestion for a completed scraper job',
7
+ builder: (y) =>
8
+ y.option('job-id', {
9
+ describe: 'Scraper job ID to ingest',
10
+ type: 'string',
11
+ demandOption: true,
12
+ }),
13
+
14
+ handler: runCommand(async (argv) => {
15
+ // Single credential resolution — avoids double readConfig() from calling readConfig()
16
+ // here and again inside post() → getCredentials().
17
+ const creds = await getCredentials();
18
+
19
+ console.log(`Triggering ingestion for job ${argv['job-id']} on site ${creds.siteId}...`);
20
+
21
+ const result = await post('/AzureIndexes/IngestJobDirect', {
22
+ siteId: creds.siteId,
23
+ jobId: argv['job-id'],
24
+ });
25
+
26
+ console.log('Ingestion started.');
27
+ console.log(` Job ID: ${result.jobId}`);
28
+ console.log(` Index: ${result.physicalIndex}`);
29
+ console.log('\nEmbedding runs in the background. Check logs for completion.');
30
+ }),
31
+ };
@@ -0,0 +1,53 @@
1
+ import { requestDeviceCode, pollForToken, exchangeForVelaroToken } from '../oauth.js';
2
+ import { readConfig, writeConfig, DEFAULT_API_BASE } from '../config.js';
3
+ import { runCommand } from '../run.js';
4
+
5
+ export const loginCommand = {
6
+ command: 'login',
7
+ describe: 'Authenticate with Velaro using your browser',
8
+ builder: (y) =>
9
+ y.option('api', {
10
+ describe: 'Velaro Messaging API base URL',
11
+ default: DEFAULT_API_BASE,
12
+ type: 'string',
13
+ }),
14
+
15
+ handler: runCommand(async (argv) => {
16
+ const apiBase = argv.api;
17
+
18
+ console.log('Starting Velaro login...\n');
19
+
20
+ const deviceData = await requestDeviceCode();
21
+
22
+ console.log(` Open: ${deviceData.verification_uri}`);
23
+ console.log(` Enter: ${deviceData.user_code}\n`);
24
+ console.log('Waiting for you to complete login in your browser...');
25
+
26
+ const entraTokens = await pollForToken(deviceData.device_code, deviceData.interval ?? 5);
27
+ const velaroResult = await exchangeForVelaroToken(entraTokens.access_token, apiBase);
28
+
29
+ writeConfig({
30
+ ...readConfig(),
31
+ apiBase,
32
+ velaroToken: velaroResult.token.token,
33
+ velaroExpires: velaroResult.token.expires,
34
+ entraRefreshToken: entraTokens.refresh_token,
35
+ siteId: velaroResult.profile?.SiteId,
36
+ userName: velaroResult.profile?.Name,
37
+ });
38
+
39
+ console.log(`\nLogged in as ${velaroResult.profile?.Name ?? velaroResult.profile?.UserName}`);
40
+ console.log(`Site ID: ${velaroResult.profile?.SiteId}`);
41
+ console.log('Credentials saved to ~/.velaro/config.json');
42
+ }),
43
+ };
44
+
45
+ export const logoutCommand = {
46
+ command: 'logout',
47
+ describe: 'Clear stored credentials',
48
+ handler: runCommand(async () => {
49
+ const { clearConfig } = await import('../config.js');
50
+ clearConfig();
51
+ console.log('Logged out. Credentials removed from ~/.velaro/config.json');
52
+ }),
53
+ };
@@ -0,0 +1,63 @@
1
+ import { get, post, del } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ export const mcpKeyCommand = {
5
+ command: 'mcp-key <subcommand>',
6
+ describe: 'Manage MCP API keys (vel_live_* keys for AI skill access)',
7
+ builder: (yargs) =>
8
+ yargs
9
+ .command(mcpKeyListCommand)
10
+ .command(mcpKeyCreateCommand)
11
+ .command(mcpKeyRevokeCommand)
12
+ .demandCommand(1, 'Specify a subcommand: list, create, revoke'),
13
+ handler: () => {},
14
+ };
15
+
16
+ const mcpKeyListCommand = {
17
+ command: 'list',
18
+ describe: 'List MCP API keys for your site',
19
+ handler: runCommand(async () => {
20
+ const keys = await get('/McpApiKeys');
21
+ if (!keys?.length) { console.log('No MCP keys found.'); return; }
22
+ console.log(`Found ${keys.length} key(s):\n`);
23
+ for (const k of keys) {
24
+ const used = k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : 'never';
25
+ console.log(` [${k.id}] ${k.label} prefix=${k.keyPrefix} ${k.isActive ? 'active' : 'revoked'} last used=${used}`);
26
+ }
27
+ }),
28
+ };
29
+
30
+ const mcpKeyCreateCommand = {
31
+ command: 'create',
32
+ describe: 'Create a new MCP API key',
33
+ builder: (y) =>
34
+ y
35
+ .option('label', {
36
+ describe: 'Human-readable label (e.g. "Claude Code")',
37
+ type: 'string',
38
+ demandOption: true,
39
+ })
40
+ .option('expires', {
41
+ describe: 'Expiry date in ISO 8601 format (e.g. 2027-01-01)',
42
+ type: 'string',
43
+ }),
44
+
45
+ handler: runCommand(async (argv) => {
46
+ const payload = { label: argv.label };
47
+ if (argv.expires) payload.expiresAt = new Date(argv.expires).toISOString();
48
+
49
+ const result = await post('/McpApiKeys', payload);
50
+ console.log(`MCP key created for: ${result.label}`);
51
+ console.log(`\n Key: ${result.rawKey}`);
52
+ console.log(`\n ${result.warning ?? 'Copy this key now — it will not be shown again.'}`);
53
+ }),
54
+ };
55
+
56
+ const mcpKeyRevokeCommand = {
57
+ command: 'revoke <id>',
58
+ describe: 'Revoke an MCP API key by ID',
59
+ handler: runCommand(async (argv) => {
60
+ await del(`/McpApiKeys/${argv.id}`);
61
+ console.log(`Key ${argv.id} revoked.`);
62
+ }),
63
+ };
@@ -0,0 +1,22 @@
1
+ import { readConfig, DEFAULT_API_BASE } from '../config.js';
2
+
3
+ export const statusCommand = {
4
+ command: 'status',
5
+ describe: 'Check Velaro API health',
6
+ handler: async () => {
7
+ const apiBase = readConfig()?.apiBase ?? DEFAULT_API_BASE;
8
+ process.stdout.write(`Checking ${apiBase}/Status ... `);
9
+ try {
10
+ const res = await fetch(`${apiBase}/Status`);
11
+ if (res.ok) {
12
+ console.log('OK');
13
+ } else {
14
+ console.log(`DEGRADED (HTTP ${res.status})`);
15
+ process.exit(1);
16
+ }
17
+ } catch (err) {
18
+ console.log(`UNREACHABLE: ${err.message}`);
19
+ process.exit(1);
20
+ }
21
+ },
22
+ };
@@ -0,0 +1,22 @@
1
+ import { readConfig } from '../config.js';
2
+
3
+ export const whoamiCommand = {
4
+ command: 'whoami',
5
+ describe: 'Show current login and site info',
6
+ handler: () => {
7
+ const cfg = readConfig();
8
+
9
+ if (!cfg?.velaroToken) {
10
+ console.error('Not logged in. Run: velaro login');
11
+ process.exit(1);
12
+ }
13
+
14
+ const expires = new Date(cfg.velaroExpires);
15
+ const minLeft = Math.round((expires - Date.now()) / 60000);
16
+
17
+ console.log(`User: ${cfg.userName ?? '(unknown)'}`);
18
+ console.log(`Site ID: ${cfg.siteId ?? '(unknown)'}`);
19
+ console.log(`API: ${cfg.apiBase}`);
20
+ console.log(`Token: ${expires > new Date() ? `valid (expires in ${minLeft}m)` : 'EXPIRED — run velaro login'}`);
21
+ },
22
+ };
package/lib/config.js ADDED
@@ -0,0 +1,25 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
4
+
5
+ const CONFIG_DIR = join(homedir(), '.velaro');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
+
8
+ export const DEFAULT_API_BASE = 'https://velaro-messaging-api.azurewebsites.net';
9
+
10
+ export function readConfig() {
11
+ try {
12
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ export function writeConfig(data) {
19
+ mkdirSync(CONFIG_DIR, { recursive: true }); // no-op if already exists
20
+ writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
21
+ }
22
+
23
+ export function clearConfig() {
24
+ writeConfig({});
25
+ }
package/lib/oauth.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * OAuth 2.0 device authorization flow (RFC 8628) against the Velaro Entra CIAM tenant.
3
+ *
4
+ * Flow:
5
+ * 1. POST /devicecode → get user_code + verification_uri
6
+ * 2. Show the user the code; poll /token until granted
7
+ * 3. Exchange the Entra access_token for a Velaro JWT via /auth/entra/token
8
+ * 4. Store { velaroToken, velaroExpires, entraRefreshToken } in ~/.velaro/config.json
9
+ *
10
+ * Refresh:
11
+ * When the Velaro JWT is within 5 minutes of expiring, silently use the stored
12
+ * Entra refresh_token to get a new access_token and re-exchange for a new Velaro JWT.
13
+ *
14
+ * One-time setup (run once in the Velaro CIAM tenant):
15
+ * az login --tenant 61de45b3-458d-49a5-913c-501247a6fe4f --allow-no-subscriptions
16
+ * az ad app create --display-name "Velaro CLI" --sign-in-audience AzureADMyOrg \
17
+ * --public-client-redirect-uris "https://login.microsoftonline.com/common/oauth2/nativeclient"
18
+ * az ad app update --id <appId> --set isFallbackPublicClient=true
19
+ * # Grant admin consent in Azure Portal:
20
+ * # api://89bd2fb7-7020-4cfe-a153-1c1ee37903c2/access_as_user (Delegated)
21
+ * # Then set VELARO_CLI_CLIENT_ID=<appId> or update the constant below.
22
+ */
23
+
24
+ const TENANT_ID = '61de45b3-458d-49a5-913c-501247a6fe4f';
25
+ const AUTHORITY = `https://login.microsoftonline.com/${TENANT_ID}`;
26
+ const CLIENT_ID = process.env.VELARO_CLI_CLIENT_ID || 'c0fdce54-e9b4-4427-a021-b2605855ff8b';
27
+
28
+ const SCOPE = [
29
+ 'api://89bd2fb7-7020-4cfe-a153-1c1ee37903c2/access_as_user',
30
+ 'offline_access',
31
+ 'openid',
32
+ 'profile',
33
+ 'email',
34
+ ].join(' ');
35
+
36
+ const DEVICE_CODE_URL = `${AUTHORITY}/oauth2/v2.0/devicecode`;
37
+ const TOKEN_URL = `${AUTHORITY}/oauth2/v2.0/token`;
38
+ const FETCH_TIMEOUT = 10_000;
39
+
40
+ export async function requestDeviceCode() {
41
+ const res = await fetch(DEVICE_CODE_URL, {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
44
+ body: new URLSearchParams({ client_id: CLIENT_ID, scope: SCOPE }),
45
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
46
+ });
47
+
48
+ const data = await res.json();
49
+ if (data.error) throw new Error(`Device code request failed: ${data.error_description || data.error}`);
50
+ return data; // { device_code, user_code, verification_uri, expires_in, interval, message }
51
+ }
52
+
53
+ export async function pollForToken(deviceCode, intervalSeconds) {
54
+ let interval = intervalSeconds;
55
+
56
+ while (true) {
57
+ const res = await fetch(TOKEN_URL, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
60
+ body: new URLSearchParams({
61
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
62
+ client_id: CLIENT_ID,
63
+ device_code: deviceCode,
64
+ }),
65
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
66
+ });
67
+
68
+ const data = await res.json();
69
+
70
+ if (data.access_token) return data; // { access_token, refresh_token, expires_in }
71
+
72
+ if (data.error === 'authorization_pending') {
73
+ // normal — keep waiting
74
+ } else if (data.error === 'slow_down') {
75
+ interval += 5 + Math.random() * 2; // RFC 8628 §3.5 — add jitter to avoid lockstep
76
+ } else if (data.error === 'expired_token') {
77
+ throw new Error('Login timed out — please run velaro login again.');
78
+ } else if (data.error === 'access_denied') {
79
+ throw new Error('Login cancelled.');
80
+ } else {
81
+ throw new Error(data.error_description || data.error);
82
+ }
83
+
84
+ await sleep(interval * 1000); // sleep at end so first attempt fires immediately
85
+ }
86
+ }
87
+
88
+ export async function exchangeForVelaroToken(entraAccessToken, apiBase) {
89
+ const res = await fetch(`${apiBase}/auth/entra/token`, {
90
+ method: 'POST',
91
+ headers: { Authorization: `Bearer ${entraAccessToken}` },
92
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
93
+ });
94
+
95
+ if (!res.ok) {
96
+ const body = await res.text().catch(() => res.status.toString());
97
+ throw new Error(`Velaro token exchange failed (${res.status}): ${body}`);
98
+ }
99
+
100
+ return res.json(); // { token: { token, expires }, profile: { SiteId, Name, UserName, ... } }
101
+ }
102
+
103
+ export async function refreshVelaroToken(credentials) {
104
+ const res = await fetch(TOKEN_URL, {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
107
+ body: new URLSearchParams({
108
+ grant_type: 'refresh_token',
109
+ client_id: CLIENT_ID,
110
+ refresh_token: credentials.entraRefreshToken,
111
+ }),
112
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
113
+ });
114
+
115
+ const data = await res.json();
116
+ if (!data.access_token) {
117
+ throw new Error(`Token refresh failed: ${data.error_description || data.error}`);
118
+ }
119
+
120
+ const velaro = await exchangeForVelaroToken(data.access_token, credentials.apiBase);
121
+
122
+ return {
123
+ ...credentials,
124
+ velaroToken: velaro.token.token,
125
+ velaroExpires: velaro.token.expires,
126
+ entraRefreshToken: data.refresh_token || credentials.entraRefreshToken,
127
+ siteId: velaro.profile?.SiteId ?? credentials.siteId,
128
+ userName: velaro.profile?.Name ?? credentials.userName,
129
+ };
130
+ }
131
+
132
+ function sleep(ms) {
133
+ return new Promise((resolve) => setTimeout(resolve, ms));
134
+ }
package/lib/run.js ADDED
@@ -0,0 +1,11 @@
1
+ /** Wraps a yargs command handler with consistent error reporting. */
2
+ export function runCommand(fn) {
3
+ return async (argv) => {
4
+ try {
5
+ await fn(argv);
6
+ } catch (err) {
7
+ console.error(`Error: ${err.message}`);
8
+ process.exit(1);
9
+ }
10
+ };
11
+ }
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@velaro/cli",
3
+ "version": "0.1.0",
4
+ "description": "Velaro Workspace v20 — command-line interface for managing bots, knowledge base ingestion, and MCP API keys.",
5
+ "type": "module",
6
+ "bin": {
7
+ "velaro": "bin/velaro.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18.0.0"
11
+ },
12
+ "dependencies": {
13
+ "yargs": "^17.7.2"
14
+ },
15
+ "files": [
16
+ "bin/",
17
+ "lib/"
18
+ ]
19
+ }