@velaro/cli 0.4.0 → 0.7.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.
@@ -0,0 +1,212 @@
1
+ import { get, post, getCredentials } from '../api.js';
2
+
3
+ // ── helpers ──────────────────────────────────────────────────────────────────
4
+
5
+ function isVelaroAdmin(creds) {
6
+ return creds.siteId === 1032;
7
+ }
8
+
9
+ function mb(n) {
10
+ if (n >= 1024) return `${(n / 1024).toFixed(1)} GB`;
11
+ return `${n} MB`;
12
+ }
13
+
14
+ function bar(used, total, width = 20) {
15
+ if (!total) return '[ ] n/a';
16
+ const pct = Math.min(used / total, 1);
17
+ const filled = Math.round(pct * width);
18
+ const color = pct >= 0.9 ? '\x1b[31m' : pct >= 0.7 ? '\x1b[33m' : '\x1b[32m';
19
+ return `${color}[${'█'.repeat(filled)}${' '.repeat(width - filled)}]\x1b[0m ${(pct * 100).toFixed(0)}%`;
20
+ }
21
+
22
+ // ── list ─────────────────────────────────────────────────────────────────────
23
+
24
+ const listCommand = {
25
+ command: 'list',
26
+ describe: 'List indexes. Velaro admins see all physical indexes; customers see their virtual indexes + quota.',
27
+ builder: (y) =>
28
+ y.option('site', { type: 'number', describe: 'Site ID (Velaro staff only)' }),
29
+ handler: async (argv) => {
30
+ const creds = await getCredentials();
31
+
32
+ if (isVelaroAdmin(creds)) {
33
+ // ── Velaro admin: show all physical indexes with storage health ─────────
34
+ const stats = await get('/DatabaseTool/SearchIndexStats');
35
+
36
+ console.log('\n\x1b[1mPhysical Indexes\x1b[0m');
37
+ console.log(` Shared: ${stats.sharedIndexes.length} Dedicated: ${stats.dedicatedIndexes.length} Total: ${stats.totalSlots}`);
38
+
39
+ const LIMIT_MB = 160 * 1024;
40
+
41
+ console.log('\n\x1b[1mShared Indexes\x1b[0m');
42
+ for (const idx of stats.sharedIndexes) {
43
+ console.log(`\n \x1b[36m${idx.indexName}\x1b[0m ${idx.sites?.length ?? 0} sites ${idx.totalDocuments.toLocaleString()} docs`);
44
+ console.log(` Storage: ${bar(idx.storageMb, LIMIT_MB)} ${mb(idx.storageMb)} / ${mb(LIMIT_MB)}`);
45
+ if (idx.sites?.length) {
46
+ for (const s of idx.sites.slice(0, 10)) {
47
+ const badge = s.assignmentType === 'legacy-fallback' ? '\x1b[33m[legacy]\x1b[0m' :
48
+ s.assignmentType === 'dedicated' ? '\x1b[35m[dedicated]\x1b[0m' :
49
+ '\x1b[34m[shared]\x1b[0m';
50
+ console.log(` Site ${s.siteId.toString().padEnd(8)} ${badge} ${s.documentCount.toLocaleString()} docs`);
51
+ }
52
+ if (idx.sites.length > 10) console.log(` ... and ${idx.sites.length - 10} more`);
53
+ }
54
+ }
55
+
56
+ if (stats.dedicatedIndexes.length) {
57
+ console.log('\n\x1b[1mDedicated Indexes\x1b[0m');
58
+ for (const idx of stats.dedicatedIndexes) {
59
+ console.log(` \x1b[35m${idx.indexName}\x1b[0m Site ${idx.siteId} ${idx.documentCount.toLocaleString()} docs`);
60
+ console.log(` Storage: ${bar(idx.storageMb, LIMIT_MB)} ${mb(idx.storageMb)} / ${mb(LIMIT_MB)}`);
61
+ }
62
+ }
63
+
64
+ const legacy = stats.sharedIndexes.flatMap(s => s.sites ?? []).filter(s => s.assignmentType === 'legacy-fallback').length;
65
+ if (legacy > 0) {
66
+ console.log(`\n\x1b[33m⚠ ${legacy} legacy site(s) have no index assignment (fallback to velaro-shared-index)\x1b[0m`);
67
+ }
68
+ } else {
69
+ // ── Customer: show their virtual indexes + quota ────────────────────────
70
+ const siteId = argv.site ?? creds.siteId;
71
+ const [indexes, breakdown] = await Promise.all([
72
+ get('/AzureIndexes/List'),
73
+ get(`/DatabaseTool/SearchIndexSite?siteId=${siteId}`).catch(() => null),
74
+ ]);
75
+
76
+ // Quota from subscription
77
+ const sub = await get('/Subscription/Get').catch(() => null);
78
+ const chunkCap = sub?.maxIndexedChunkTotal ?? 0;
79
+ const usedChunks = indexes.reduce((s, i) => s + (i.documentCount ?? 0), 0);
80
+
81
+ console.log('\n\x1b[1mYour Search Indexes\x1b[0m');
82
+
83
+ if (chunkCap > 0) {
84
+ console.log(`\nQuota: ${bar(usedChunks, chunkCap)} ${usedChunks.toLocaleString()} / ${chunkCap.toLocaleString()} chunks used`);
85
+ const remaining = chunkCap - usedChunks;
86
+ if (remaining <= 0) {
87
+ console.log('\x1b[31m✗ Quota full — new content will not be indexed until you upgrade or remove content.\x1b[0m');
88
+ } else if (remaining < chunkCap * 0.1) {
89
+ console.log(`\x1b[33m⚠ Only ${remaining.toLocaleString()} chunks remaining. Consider upgrading your plan.\x1b[0m`);
90
+ }
91
+ }
92
+
93
+ if (breakdown) {
94
+ const typeLabel = breakdown.assignmentType === 'dedicated' ? '\x1b[35m[dedicated index]\x1b[0m' :
95
+ breakdown.assignmentType === 'shared' ? '\x1b[34m[shared index]\x1b[0m' :
96
+ '\x1b[33m[legacy]\x1b[0m';
97
+ console.log(`\nPhysical index: \x1b[36m${breakdown.physicalIndex}\x1b[0m ${typeLabel}`);
98
+ }
99
+
100
+ console.log('');
101
+ for (const idx of indexes) {
102
+ const docs = idx.documentCount != null ? `${idx.documentCount.toLocaleString()} docs` : '';
103
+ console.log(` \x1b[36m${idx.name}\x1b[0m (id: ${idx.id}) ${docs}`);
104
+ if (idx.description) console.log(` ${idx.description}`);
105
+ }
106
+
107
+ if (!indexes.length) console.log(' No indexes found. Create one in your bot settings.');
108
+ }
109
+ console.log('');
110
+ },
111
+ };
112
+
113
+ // ── site (Velaro staff only) ──────────────────────────────────────────────────
114
+
115
+ const siteCommand = {
116
+ command: 'site <siteId>',
117
+ describe: 'Show index breakdown for a specific site (Velaro staff only)',
118
+ handler: async (argv) => {
119
+ const creds = await getCredentials();
120
+ if (!isVelaroAdmin(creds)) {
121
+ console.error('This command is only available to Velaro staff.');
122
+ process.exit(1);
123
+ }
124
+ const data = await get(`/DatabaseTool/SearchIndexSite?siteId=${argv.siteId}`);
125
+ console.log(`\nSite ${argv.siteId} → \x1b[36m${data.physicalIndex}\x1b[0m [${data.assignmentType}]`);
126
+ console.log(`Total docs: ${data.totalDocuments.toLocaleString()}\n`);
127
+ for (const t of data.indexTypes) {
128
+ console.log(` ${t.indexName.padEnd(40)} ${t.count.toLocaleString()} docs`);
129
+ }
130
+ console.log('');
131
+ },
132
+ };
133
+
134
+ // ── provision (Velaro staff only) ─────────────────────────────────────────────
135
+
136
+ const provisionCommand = {
137
+ command: 'provision <siteId>',
138
+ describe: 'Provision a dedicated physical index for a site (Velaro staff only)',
139
+ handler: async (argv) => {
140
+ const creds = await getCredentials();
141
+ if (!isVelaroAdmin(creds)) {
142
+ console.error('This command is only available to Velaro staff.');
143
+ process.exit(1);
144
+ }
145
+ console.log(`Provisioning dedicated index for site ${argv.siteId}...`);
146
+ const result = await post('/DatabaseTool/ProvisionDedicatedIndex', { SiteId: argv.siteId });
147
+ console.log(`\x1b[32m✓ ${result.message}\x1b[0m`);
148
+ console.log(` Index: \x1b[36m${result.indexName}\x1b[0m`);
149
+ console.log('\nNote: Re-ingest content to populate the new index.');
150
+ },
151
+ };
152
+
153
+ // ── reingest (customer-facing, credit-gated) ──────────────────────────────────
154
+
155
+ const reingestCommand = {
156
+ command: 'reingest',
157
+ describe: 'Re-ingest all content sources for your site (requires available index quota)',
158
+ builder: (y) =>
159
+ y
160
+ .option('source', {
161
+ type: 'string',
162
+ describe: 'Specific source to reingest: kb | shopify | bigcommerce | scraper',
163
+ })
164
+ .option('site', {
165
+ type: 'number',
166
+ describe: 'Site ID (Velaro staff only)',
167
+ }),
168
+ handler: async (argv) => {
169
+ const creds = await getCredentials();
170
+
171
+ // Check quota before allowing reingest
172
+ const sub = await get('/Subscription/Get').catch(() => null);
173
+ if (!isVelaroAdmin(creds) && sub?.maxIndexedChunkTotal > 0) {
174
+ const indexes = await get('/AzureIndexes/List').catch(() => []);
175
+ const usedChunks = indexes.reduce((s, i) => s + (i.documentCount ?? 0), 0);
176
+ if (usedChunks >= sub.maxIndexedChunkTotal) {
177
+ console.error(`\x1b[31m✗ Index quota full (${usedChunks.toLocaleString()} / ${sub.maxIndexedChunkTotal.toLocaleString()} chunks).\x1b[0m`);
178
+ console.error(' Upgrade your plan or remove content before reingesting.');
179
+ process.exit(1);
180
+ }
181
+ }
182
+
183
+ const sources = argv.source ? [argv.source] : ['kb', 'shopify', 'bigcommerce', 'scraper'];
184
+ const siteParam = isVelaroAdmin(creds) && argv.site ? `?siteId=${argv.site}` : '';
185
+
186
+ for (const source of sources) {
187
+ try {
188
+ console.log(` Syncing ${source}...`);
189
+ await post(`/AzureIndexes/SyncIntegration${siteParam}`, { source, forceAll: true });
190
+ console.log(` \x1b[32m✓ ${source} queued\x1b[0m`);
191
+ } catch (e) {
192
+ console.log(` \x1b[33m⚠ ${source} skipped: ${e.message}\x1b[0m`);
193
+ }
194
+ }
195
+ console.log('\nReingest queued. Run `velaro index list` to monitor progress.');
196
+ },
197
+ };
198
+
199
+ // ── export ────────────────────────────────────────────────────────────────────
200
+
201
+ export const indexCommand = {
202
+ command: 'index <subcommand>',
203
+ describe: 'Manage search indexes — view quota, physical index health, and reingest content',
204
+ builder: (yargs) =>
205
+ yargs
206
+ .command(listCommand)
207
+ .command(siteCommand)
208
+ .command(provisionCommand)
209
+ .command(reingestCommand)
210
+ .demandCommand(1, 'Specify a subcommand: list, site, provision, reingest'),
211
+ handler: () => {},
212
+ };
@@ -1,31 +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
- };
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
+ };
@@ -14,10 +14,78 @@ 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 KB articles or scraper/ingestion content (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
+ .option('ingestion', {
41
+ type: 'boolean',
42
+ describe: 'Reindex scraper/ingestion Azure Search indexers instead of KB articles',
43
+ default: false,
44
+ }),
45
+ handler: runCommand(async (argv) => {
46
+ if (argv.ingestion) {
47
+ if (argv.all) {
48
+ console.log('Starting bulk ingestion reindex for all sites — runs in background.');
49
+ console.log('Watch server logs ([ReindexAllIngestion]) for progress.\n');
50
+ const result = await post('/DatabaseTool/ReindexAllIngestionIndexes', {});
51
+ console.log(result.message ?? 'Queued.');
52
+ } else if (argv.site) {
53
+ console.log(`Reindexing scraper/ingestion index for site ${argv.site}…`);
54
+ const result = await post(`/DatabaseTool/ReindexIngestionIndex/${argv.site}`, {});
55
+ if (result.success) {
56
+ console.log(`✅ Triggered ${result.indexersRun} indexer(s) — re-processing in background.`);
57
+ } else {
58
+ console.error(`❌ Failed: ${result.message}`);
59
+ }
60
+ } else {
61
+ console.error('--ingestion requires --site <id> or --all (Velaro staff only)');
62
+ process.exit(1);
63
+ }
64
+ } else if (argv.all) {
65
+ console.log('Starting bulk KB reindex for all sites — this runs in the background.');
66
+ console.log('Watch server logs ([ReindexAllKb]) for progress.\n');
67
+ const result = await post('/DatabaseTool/ReindexAllKb', {});
68
+ console.log(result.message ?? 'Queued.');
69
+ } else if (argv.site) {
70
+ console.log(`Reindexing KB articles for site ${argv.site}…`);
71
+ const result = await post(`/DatabaseTool/ReindexKb/${argv.site}`, {});
72
+ if (result.success) {
73
+ console.log(`✅ Done — ${result.articles} articles reindexed.`);
74
+ } else {
75
+ console.error(`❌ Failed: ${result.message}`);
76
+ }
77
+ } else {
78
+ console.log("Reindexing your site's KB articles\u2026");
79
+ const result = await post('/KBSearchIndex/reindex', {});
80
+ if (result.success === false) {
81
+ console.error(`❌ Failed: ${result.message}`);
82
+ } else {
83
+ console.log(`✅ Done — ${result.articles ?? result.count ?? 'all'} articles reindexed.`);
84
+ }
85
+ }
86
+ }),
87
+ };
88
+
21
89
  // ────────────────────────────────────────────────────────────────────────────
22
90
  // Q&A
23
91
  // ────────────────────────────────────────────────────────────────────────────
@@ -1,27 +1,27 @@
1
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';
2
+ import { readConfig, writeConfig, setEnvCredentials, getActiveEnv, ENVS } from '../config.js';
3
+ import { runCommand } from '../run.js';
4
4
 
5
5
  export const loginCommand = {
6
6
  command: 'login',
7
- describe: 'Authenticate with Velaro using your browser',
7
+ describe: 'Authenticate with Velaro (saves credentials for the chosen environment)',
8
8
  builder: (y) =>
9
9
  y
10
10
  .option('staging', {
11
- describe: 'Connect to the Velaro staging environment',
11
+ describe: 'Log in to the staging environment',
12
12
  type: 'boolean',
13
13
  default: false,
14
14
  })
15
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.',
16
+ describe: 'Override the admin API base URL (advanced)',
18
17
  type: 'string',
19
18
  }),
20
19
 
21
20
  handler: runCommand(async (argv) => {
22
- const apiBase = argv.api ?? (argv.staging ? STAGING_API_BASE : DEFAULT_API_BASE);
21
+ const env = argv.staging || argv.api?.includes('staging') ? 'staging' : 'prod';
22
+ const adminApiBase = argv.api || ENVS[env].adminApiBase;
23
23
 
24
- console.log('Starting Velaro login...\n');
24
+ console.log(`Starting Velaro login (${env})...\n`);
25
25
 
26
26
  const deviceData = await requestDeviceCode();
27
27
 
@@ -30,11 +30,10 @@ export const loginCommand = {
30
30
  console.log('Waiting for you to complete login in your browser...');
31
31
 
32
32
  const entraTokens = await pollForToken(deviceData.device_code, deviceData.interval ?? 5);
33
- const velaroResult = await exchangeForVelaroToken(entraTokens.access_token, apiBase);
33
+ const velaroResult = await exchangeForVelaroToken(entraTokens.access_token, adminApiBase);
34
34
 
35
- writeConfig({
36
- ...readConfig(),
37
- apiBase,
35
+ setEnvCredentials(env, {
36
+ adminApiBase,
38
37
  velaroToken: velaroResult.token.token,
39
38
  velaroExpires: velaroResult.token.expires,
40
39
  entraRefreshToken: entraTokens.refresh_token,
@@ -42,19 +41,46 @@ export const loginCommand = {
42
41
  userName: velaroResult.profile?.Name,
43
42
  });
44
43
 
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');
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.');
49
55
  }),
50
56
  };
51
57
 
52
58
  export const logoutCommand = {
53
59
  command: 'logout',
54
60
  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');
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
+ }
59
85
  }),
60
86
  };
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import { get, post, del } from '../api.js';
5
+ import { getActiveEnv, ENVS } from '../config.js';
5
6
  import { runCommand } from '../run.js';
6
7
 
7
8
  export const mcpKeyCommand = {
@@ -76,28 +77,34 @@ const mcpKeyInstallCommand = {
76
77
  .option('label', {
77
78
  describe: 'Label for the key',
78
79
  type: 'string',
79
- default: 'Claude Code',
80
80
  })
81
- .option('api', {
82
- describe: 'Velaro API base URL (defaults to production)',
83
- type: 'string',
81
+ .option('staging', {
82
+ describe: 'Install key for the staging environment',
83
+ type: 'boolean',
84
+ default: false,
84
85
  }),
85
86
 
86
87
  handler: runCommand(async (argv) => {
87
- const payload = { label: argv.label };
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 };
88
93
  const result = await post('/McpApiKeys', payload);
89
94
  const rawKey = result.rawKey;
90
95
 
91
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}`;
92
100
  const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
93
101
  let settings = {};
94
102
  if (fs.existsSync(settingsPath)) {
95
103
  try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
96
104
  }
97
105
 
98
- const apiBase = argv.api || 'https://api-admin-us-east.velaro.com';
99
106
  settings.mcpServers = settings.mcpServers || {};
100
- settings.mcpServers.velaro = {
107
+ settings.mcpServers[serverName] = {
101
108
  command: 'npx',
102
109
  args: ['-y', '@velaro/mcp-server'],
103
110
  env: {
@@ -109,10 +116,10 @@ const mcpKeyInstallCommand = {
109
116
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
110
117
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
111
118
 
112
- console.log(`✅ MCP key created: ${result.label} (${result.keyPrefix}...)`);
113
- console.log(`✅ Written to: ${settingsPath}`);
119
+ console.log(`✅ MCP key created: ${label} (${result.keyPrefix}...)`);
120
+ console.log(`✅ Written to settings.json as "${serverName}" → ${apiBase}`);
114
121
  console.log(`\nRestart Claude Code to pick up the new MCP server.`);
115
- console.log(`\nTo verify: run 'velaro mcp-key list' or check Settings Developer → MCP Keys in the admin portal.`);
122
+ console.log(`\nTo use in Claude Code: say "use ${serverName}" to target ${env}.`);
116
123
  }),
117
124
  };
118
125