@velaro/cli 0.3.0 → 0.5.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/bin/velaro.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import yargs from 'yargs';
4
4
  import { hideBin } from 'yargs/helpers';
5
5
  import { loginCommand, logoutCommand } from '../lib/commands/login.js';
6
+ import { envCommand } from '../lib/commands/env.js';
6
7
  import { whoamiCommand } from '../lib/commands/whoami.js';
7
8
  import { teamCommand } from '../lib/commands/team.js';
8
9
  import { botCommand } from '../lib/commands/bot.js';
@@ -28,6 +29,7 @@ await yargs(hideBin(process.argv))
28
29
  .command(loginCommand)
29
30
  .command(logoutCommand)
30
31
  .command(whoamiCommand)
32
+ .command(envCommand)
31
33
  // Info
32
34
  .command(siteCommand)
33
35
  .command(checkCommand)
package/lib/api.js CHANGED
@@ -1,30 +1,34 @@
1
- import { readConfig, writeConfig } from './config.js';
2
- import { refreshVelaroToken } from './oauth.js';
1
+ import { readConfig, writeConfig, getActiveEnv, setEnvCredentials, ENVS } from './config.js';
2
+ import { refreshVelaroToken } from './oauth.js';
3
3
 
4
4
  const REFRESH_BUFFER_MS = 5 * 60 * 1000;
5
5
 
6
- export async function getCredentials() {
7
- let creds = readConfig();
6
+ export async function getCredentials(envOverride) {
7
+ const cfg = readConfig();
8
+ const env = envOverride || cfg.activeEnv || 'prod';
9
+ let creds = cfg.envs?.[env];
8
10
 
9
11
  if (!creds?.velaroToken) {
10
- throw new Error('Not logged in. Run: velaro login');
12
+ throw new Error(`Not logged in to ${env}. Run: velaro login${env === 'staging' ? ' --staging' : ''}`);
11
13
  }
12
14
 
13
15
  if (Date.now() + REFRESH_BUFFER_MS >= new Date(creds.velaroExpires).getTime()) {
14
16
  if (!creds.entraRefreshToken) {
15
- throw new Error('Session expired. Run: velaro login');
17
+ throw new Error(`Session expired for ${env}. Run: velaro login${env === 'staging' ? ' --staging' : ''}`);
16
18
  }
17
- creds = await refreshVelaroToken(creds);
18
- writeConfig(creds);
19
+ const refreshed = await refreshVelaroToken({ ...creds, apiBase: creds.adminApiBase });
20
+ const updated = { ...creds, ...refreshed };
21
+ setEnvCredentials(env, updated);
22
+ creds = updated;
19
23
  }
20
24
 
21
- return creds;
25
+ return { ...creds, env };
22
26
  }
23
27
 
24
- export async function request(method, path, body) {
25
- const creds = await getCredentials();
28
+ export async function request(method, path, body, envOverride) {
29
+ const creds = await getCredentials(envOverride);
26
30
 
27
- const res = await fetch(`${creds.apiBase}${path}`, {
31
+ const res = await fetch(`${creds.adminApiBase}${path}`, {
28
32
  method,
29
33
  headers: {
30
34
  Authorization: `Bearer ${creds.velaroToken}`,
@@ -35,7 +39,7 @@ export async function request(method, path, body) {
35
39
 
36
40
  if (!res.ok) {
37
41
  let msg = `${method} ${path} → ${res.status}`;
38
- try { const t = await res.text(); if (t) msg += `: ${t}`; } catch { /* body unreadable — status already captured */ }
42
+ try { const t = await res.text(); if (t) msg += `: ${t}`; } catch { /* status already captured */ }
39
43
  throw new Error(msg);
40
44
  }
41
45
 
@@ -0,0 +1,45 @@
1
+ import { readConfig, setActiveEnv, getActiveEnv, ENVS } from '../config.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ export const envCommand = {
5
+ command: 'env [name]',
6
+ describe: 'Show or switch the active environment (prod/staging)',
7
+ builder: (y) =>
8
+ y.positional('name', {
9
+ describe: 'Environment to switch to: prod or staging',
10
+ type: 'string',
11
+ choices: ['prod', 'staging'],
12
+ }),
13
+
14
+ handler: runCommand(async (argv) => {
15
+ if (argv.name) {
16
+ setActiveEnv(argv.name);
17
+ console.log(`Switched to ${argv.name}.`);
18
+ console.log(`All velaro commands now target: ${ENVS[argv.name].adminApiBase}`);
19
+ return;
20
+ }
21
+
22
+ // Show status of all environments
23
+ const cfg = readConfig();
24
+ const active = cfg.activeEnv || 'prod';
25
+ const envs = cfg.envs || {};
26
+
27
+ console.log('Velaro environments:\n');
28
+ for (const [name, urls] of Object.entries(ENVS)) {
29
+ const creds = envs[name];
30
+ const marker = name === active ? '▶ ' : ' ';
31
+ const status = creds?.velaroToken
32
+ ? `logged in as ${creds.userName ?? 'unknown'} (site ${creds.siteId})`
33
+ : 'not logged in';
34
+ const expiry = creds?.velaroExpires
35
+ ? new Date(creds.velaroExpires) > new Date() ? '' : ' ⚠ token expired'
36
+ : '';
37
+ console.log(`${marker}${name.padEnd(10)} ${status}${expiry}`);
38
+ console.log(` ${urls.adminApiBase}`);
39
+ }
40
+
41
+ console.log(`\nActive: ${active}`);
42
+ console.log('\nTo switch: velaro env staging | velaro env prod');
43
+ console.log('To log in: velaro login | velaro login --staging');
44
+ }),
45
+ };
@@ -14,10 +14,55 @@ export const kbCommand = {
14
14
  .command(qnaCommand)
15
15
  .command(overrideCommand)
16
16
  .command(contentCommand)
17
- .demandCommand(1, 'Specify a subcommand: article, qna, override, content'),
17
+ .command(reindexCommand)
18
+ .demandCommand(1, 'Specify a subcommand: article, qna, override, content, reindex'),
18
19
  handler: () => {},
19
20
  };
20
21
 
22
+ // ────────────────────────────────────────────────────────────────────────────
23
+ // Reindex — re-embed all KB articles with the current embedding model
24
+ // ────────────────────────────────────────────────────────────────────────────
25
+
26
+ const reindexCommand = {
27
+ command: 'reindex',
28
+ describe: 'Re-embed all KB articles into the vector index (use after embedding model upgrade)',
29
+ builder: (y) =>
30
+ y
31
+ .option('all', {
32
+ type: 'boolean',
33
+ describe: 'Reindex ALL sites (Velaro staff only)',
34
+ default: false,
35
+ })
36
+ .option('site', {
37
+ type: 'number',
38
+ describe: 'Reindex a specific site by ID (Velaro staff only)',
39
+ }),
40
+ handler: runCommand(async (argv) => {
41
+ if (argv.all) {
42
+ console.log('Starting bulk reindex for all sites — this runs in the background.');
43
+ console.log('Watch server logs ([ReindexAllKb]) for progress.\n');
44
+ const result = await post('/DatabaseTool/ReindexAllKb', {});
45
+ console.log(result.message ?? 'Queued.');
46
+ } else if (argv.site) {
47
+ console.log(`Reindexing site ${argv.site}…`);
48
+ const result = await post(`/DatabaseTool/ReindexKb/${argv.site}`, {});
49
+ if (result.success) {
50
+ console.log(`✅ Done — ${result.articles} articles reindexed.`);
51
+ } else {
52
+ console.error(`❌ Failed: ${result.message}`);
53
+ }
54
+ } else {
55
+ console.log("Reindexing your site's KB articles\u2026");
56
+ const result = await post('/KBSearchIndex/reindex', {});
57
+ if (result.success === false) {
58
+ console.error(`❌ Failed: ${result.message}`);
59
+ } else {
60
+ console.log(`✅ Done — ${result.articles ?? result.count ?? 'all'} articles reindexed.`);
61
+ }
62
+ }
63
+ }),
64
+ };
65
+
21
66
  // ────────────────────────────────────────────────────────────────────────────
22
67
  // Q&A
23
68
  // ────────────────────────────────────────────────────────────────────────────
@@ -1,60 +1,86 @@
1
- import { requestDeviceCode, pollForToken, exchangeForVelaroToken } from '../oauth.js';
2
- import { readConfig, writeConfig, DEFAULT_API_BASE, STAGING_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
10
- .option('staging', {
11
- describe: 'Connect to the Velaro staging environment',
12
- type: 'boolean',
13
- default: false,
14
- })
15
- .option('api', {
16
- describe: 'Override the Velaro API base URL. Set VELARO_API_BASE env var to make it permanent. ' +
17
- 'Production cutover: set this to your production API URL before going live.',
18
- type: 'string',
19
- }),
20
-
21
- handler: runCommand(async (argv) => {
22
- const apiBase = argv.api ?? (argv.staging ? STAGING_API_BASE : DEFAULT_API_BASE);
23
-
24
- console.log('Starting Velaro login...\n');
25
-
26
- const deviceData = await requestDeviceCode();
27
-
28
- console.log(` Open: ${deviceData.verification_uri}`);
29
- console.log(` Enter: ${deviceData.user_code}\n`);
30
- console.log('Waiting for you to complete login in your browser...');
31
-
32
- const entraTokens = await pollForToken(deviceData.device_code, deviceData.interval ?? 5);
33
- const velaroResult = await exchangeForVelaroToken(entraTokens.access_token, apiBase);
34
-
35
- writeConfig({
36
- ...readConfig(),
37
- apiBase,
38
- velaroToken: velaroResult.token.token,
39
- velaroExpires: velaroResult.token.expires,
40
- entraRefreshToken: entraTokens.refresh_token,
41
- siteId: velaroResult.profile?.SiteId,
42
- userName: velaroResult.profile?.Name,
43
- });
44
-
45
- const envLabel = argv.staging || argv.api ? ` (${apiBase})` : '';
46
- console.log(`\nLogged in as ${velaroResult.profile?.Name ?? velaroResult.profile?.UserName}${envLabel}`);
47
- console.log(`Site ID: ${velaroResult.profile?.SiteId}`);
48
- console.log('Credentials saved to ~/.velaro/config.json');
49
- }),
50
- };
51
-
52
- export const logoutCommand = {
53
- command: 'logout',
54
- describe: 'Clear stored credentials',
55
- handler: runCommand(async () => {
56
- const { clearConfig } = await import('../config.js');
57
- clearConfig();
58
- console.log('Logged out. Credentials removed from ~/.velaro/config.json');
59
- }),
60
- };
1
+ import { requestDeviceCode, pollForToken, exchangeForVelaroToken } from '../oauth.js';
2
+ import { readConfig, writeConfig, setEnvCredentials, getActiveEnv, ENVS } from '../config.js';
3
+ import { runCommand } from '../run.js';
4
+
5
+ export const loginCommand = {
6
+ command: 'login',
7
+ describe: 'Authenticate with Velaro (saves credentials for the chosen environment)',
8
+ builder: (y) =>
9
+ y
10
+ .option('staging', {
11
+ describe: 'Log in to the staging environment',
12
+ type: 'boolean',
13
+ default: false,
14
+ })
15
+ .option('api', {
16
+ describe: 'Override the admin API base URL (advanced)',
17
+ type: 'string',
18
+ }),
19
+
20
+ handler: runCommand(async (argv) => {
21
+ const env = argv.staging || argv.api?.includes('staging') ? 'staging' : 'prod';
22
+ const adminApiBase = argv.api || ENVS[env].adminApiBase;
23
+
24
+ console.log(`Starting Velaro login (${env})...\n`);
25
+
26
+ const deviceData = await requestDeviceCode();
27
+
28
+ console.log(` Open: ${deviceData.verification_uri}`);
29
+ console.log(` Enter: ${deviceData.user_code}\n`);
30
+ console.log('Waiting for you to complete login in your browser...');
31
+
32
+ const entraTokens = await pollForToken(deviceData.device_code, deviceData.interval ?? 5);
33
+ const velaroResult = await exchangeForVelaroToken(entraTokens.access_token, adminApiBase);
34
+
35
+ setEnvCredentials(env, {
36
+ adminApiBase,
37
+ velaroToken: velaroResult.token.token,
38
+ velaroExpires: velaroResult.token.expires,
39
+ entraRefreshToken: entraTokens.refresh_token,
40
+ siteId: velaroResult.profile?.SiteId,
41
+ userName: velaroResult.profile?.Name,
42
+ });
43
+
44
+ // Set as active if it's the only env logged in, or if explicitly chosen
45
+ const cfg = readConfig();
46
+ if (!cfg.activeEnv || Object.keys(cfg.envs || {}).length === 1) {
47
+ cfg.activeEnv = env;
48
+ writeConfig(cfg);
49
+ }
50
+
51
+ console.log(`\n✅ Logged in to ${env} as ${velaroResult.profile?.Name ?? velaroResult.profile?.UserName}`);
52
+ console.log(` Site ID: ${velaroResult.profile?.SiteId}`);
53
+ console.log(` API: ${adminApiBase}`);
54
+ console.log('\nCredentials saved. Run "velaro env" to see all environments.');
55
+ }),
56
+ };
57
+
58
+ export const logoutCommand = {
59
+ command: 'logout',
60
+ describe: 'Clear stored credentials',
61
+ builder: (y) =>
62
+ y.option('staging', { describe: 'Log out of staging only', type: 'boolean', default: false })
63
+ .option('all', { describe: 'Log out of all environments', type: 'boolean', default: false }),
64
+
65
+ handler: runCommand(async (argv) => {
66
+ const { readConfig, writeConfig, clearConfig } = await import('../config.js');
67
+ if (argv.all) {
68
+ clearConfig();
69
+ console.log('Logged out of all environments.');
70
+ return;
71
+ }
72
+ const env = argv.staging ? 'staging' : getActiveEnv();
73
+ const cfg = readConfig();
74
+ if (cfg.envs?.[env]) {
75
+ delete cfg.envs[env];
76
+ if (cfg.activeEnv === env) {
77
+ const remaining = Object.keys(cfg.envs || {});
78
+ cfg.activeEnv = remaining[0] || 'prod';
79
+ }
80
+ writeConfig(cfg);
81
+ console.log(`Logged out of ${env}.`);
82
+ } else {
83
+ console.log(`Not logged in to ${env}.`);
84
+ }
85
+ }),
86
+ };
@@ -1,4 +1,8 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
1
4
  import { get, post, del } from '../api.js';
5
+ import { getActiveEnv, ENVS } from '../config.js';
2
6
  import { runCommand } from '../run.js';
3
7
 
4
8
  export const mcpKeyCommand = {
@@ -10,7 +14,8 @@ export const mcpKeyCommand = {
10
14
  .command(mcpKeyCreateCommand)
11
15
  .command(mcpKeyRevokeCommand)
12
16
  .command(mcpKeyRotateCommand)
13
- .demandCommand(1, 'Specify a subcommand: list, create, revoke, rotate'),
17
+ .command(mcpKeyInstallCommand)
18
+ .demandCommand(1, 'Specify a subcommand: list, create, revoke, rotate, install'),
14
19
  handler: () => {},
15
20
  };
16
21
 
@@ -64,6 +69,60 @@ const mcpKeyRevokeCommand = {
64
69
  }),
65
70
  };
66
71
 
72
+ const mcpKeyInstallCommand = {
73
+ command: 'install',
74
+ describe: 'Create an MCP key and write it to ~/.claude/settings.json automatically',
75
+ builder: (y) =>
76
+ y
77
+ .option('label', {
78
+ describe: 'Label for the key',
79
+ type: 'string',
80
+ })
81
+ .option('staging', {
82
+ describe: 'Install key for the staging environment',
83
+ type: 'boolean',
84
+ default: false,
85
+ }),
86
+
87
+ handler: runCommand(async (argv) => {
88
+ const env = argv.staging ? 'staging' : getActiveEnv();
89
+ const apiBase = ENVS[env].adminApiBase;
90
+ const label = argv.label || (env === 'staging' ? 'Claude Code (Staging)' : 'Claude Code');
91
+
92
+ const payload = { label };
93
+ const result = await post('/McpApiKeys', payload);
94
+ const rawKey = result.rawKey;
95
+
96
+ // Write to ~/.claude/settings.json
97
+ // prod → entry named "velaro"
98
+ // staging → entry named "velaro-staging"
99
+ const serverName = env === 'prod' ? 'velaro' : `velaro-${env}`;
100
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
101
+ let settings = {};
102
+ if (fs.existsSync(settingsPath)) {
103
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
104
+ }
105
+
106
+ settings.mcpServers = settings.mcpServers || {};
107
+ settings.mcpServers[serverName] = {
108
+ command: 'npx',
109
+ args: ['-y', '@velaro/mcp-server'],
110
+ env: {
111
+ VELARO_MCP_KEY: rawKey,
112
+ VELARO_ADMIN_API: apiBase,
113
+ },
114
+ };
115
+
116
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
117
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
118
+
119
+ console.log(`✅ MCP key created: ${label} (${result.keyPrefix}...)`);
120
+ console.log(`✅ Written to settings.json as "${serverName}" → ${apiBase}`);
121
+ console.log(`\nRestart Claude Code to pick up the new MCP server.`);
122
+ console.log(`\nTo use in Claude Code: say "use ${serverName}" to target ${env}.`);
123
+ }),
124
+ };
125
+
67
126
  const mcpKeyRotateCommand = {
68
127
  command: 'rotate <id>',
69
128
  describe: 'Revoke an existing key and issue a replacement in one step',
@@ -1,10 +1,12 @@
1
- import { readConfig, DEFAULT_API_BASE } from '../config.js';
1
+ import { readConfig, getActiveEnv, ENVS } from '../config.js';
2
2
 
3
3
  export const statusCommand = {
4
4
  command: 'status',
5
5
  describe: 'Check Velaro API health',
6
6
  handler: async () => {
7
- const apiBase = readConfig()?.apiBase ?? DEFAULT_API_BASE;
7
+ const cfg = readConfig();
8
+ const env = getActiveEnv();
9
+ const apiBase = cfg.envs?.[env]?.adminApiBase ?? ENVS[env].adminApiBase;
8
10
  process.stdout.write(`Checking ${apiBase}/Status ... `);
9
11
  try {
10
12
  const res = await fetch(`${apiBase}/Status`);
package/lib/config.js CHANGED
@@ -1,28 +1,83 @@
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
- // Production API URL — update when production messaging API is deployed.
9
- // messaging.velaro.com is the chat widget host (Azure SWA), not the API.
10
- export const DEFAULT_API_BASE = process.env.VELARO_API_BASE || 'https://velaro-messaging-api-staging.azurewebsites.net';
11
- export const STAGING_API_BASE = process.env.VELARO_STAGING_API || 'https://velaro-messaging-api-staging.azurewebsites.net';
12
-
13
- export function readConfig() {
14
- try {
15
- return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
16
- } catch {
17
- return {};
18
- }
19
- }
20
-
21
- export function writeConfig(data) {
22
- mkdirSync(CONFIG_DIR, { recursive: true }); // no-op if already exists
23
- writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
24
- }
25
-
26
- export function clearConfig() {
27
- writeConfig({});
28
- }
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 ENVS = {
9
+ prod: {
10
+ adminApiBase: 'https://api-admin-us-east.velaro.com',
11
+ messagingApiBase: 'https://velaro-messaging-api-staging.azurewebsites.net',
12
+ },
13
+ staging: {
14
+ adminApiBase: 'https://velaro-admin-staging.azurewebsites.net',
15
+ messagingApiBase: 'https://velaro-messaging-api-staging.azurewebsites.net',
16
+ },
17
+ };
18
+
19
+ // Legacy single-entry config — migrate transparently on first read.
20
+ function migrate(raw) {
21
+ if (raw.envs) return raw; // already multi-env
22
+ const apiBase = raw.apiBase || '';
23
+ const env = apiBase.includes('staging') ? 'staging' : 'prod';
24
+ return {
25
+ activeEnv: env,
26
+ envs: {
27
+ [env]: {
28
+ adminApiBase: ENVS[env].adminApiBase,
29
+ messagingApiBase: ENVS[env].messagingApiBase,
30
+ velaroToken: raw.velaroToken,
31
+ velaroExpires: raw.velaroExpires,
32
+ entraRefreshToken: raw.entraRefreshToken,
33
+ siteId: raw.siteId,
34
+ userName: raw.userName,
35
+ },
36
+ },
37
+ _lastUpdateCheck: raw._lastUpdateCheck,
38
+ _latestVersion: raw._latestVersion,
39
+ };
40
+ }
41
+
42
+ export function readConfig() {
43
+ try {
44
+ const raw = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
45
+ return migrate(raw);
46
+ } catch {
47
+ return { activeEnv: 'prod', envs: {} };
48
+ }
49
+ }
50
+
51
+ export function writeConfig(data) {
52
+ mkdirSync(CONFIG_DIR, { recursive: true });
53
+ writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
54
+ }
55
+
56
+ export function getActiveEnv() {
57
+ return readConfig().activeEnv || 'prod';
58
+ }
59
+
60
+ export function setActiveEnv(env) {
61
+ if (!ENVS[env]) throw new Error(`Unknown environment "${env}". Use: prod, staging`);
62
+ const cfg = readConfig();
63
+ cfg.activeEnv = env;
64
+ writeConfig(cfg);
65
+ }
66
+
67
+ export function getEnvCredentials(env) {
68
+ const cfg = readConfig();
69
+ return cfg.envs?.[env] || null;
70
+ }
71
+
72
+ export function setEnvCredentials(env, creds) {
73
+ if (!ENVS[env]) throw new Error(`Unknown environment "${env}". Use: prod, staging`);
74
+ const cfg = readConfig();
75
+ cfg.envs = cfg.envs || {};
76
+ cfg.envs[env] = { ...ENVS[env], ...creds };
77
+ cfg.activeEnv = cfg.activeEnv || env;
78
+ writeConfig(cfg);
79
+ }
80
+
81
+ export function clearConfig() {
82
+ writeConfig({ activeEnv: 'prod', envs: {} });
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Velaro Workspace v20 — command-line interface for managing bots, knowledge base ingestion, and MCP API keys.",
5
5
  "type": "module",
6
6
  "bin": {