voyageai-cli 1.1.0 → 1.2.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,353 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const pc = require('picocolors');
7
+
8
+ const CLI_PATH = path.join(__dirname, '..', 'cli.js');
9
+
10
+ /**
11
+ * Wait for the user to press Enter.
12
+ * Resolves immediately if noPause is true.
13
+ * @param {boolean} noPause
14
+ * @returns {Promise<void>}
15
+ */
16
+ function waitForEnter(noPause) {
17
+ if (noPause) return Promise.resolve();
18
+ return new Promise((resolve) => {
19
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
20
+ rl.question(pc.dim(' Press Enter to continue...'), () => {
21
+ rl.close();
22
+ resolve();
23
+ });
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Print a command that's about to be run.
29
+ * @param {string} cmd
30
+ */
31
+ function showCommand(cmd) {
32
+ console.log(`\n ${pc.bold(pc.cyan('$ ' + cmd))}\n`);
33
+ }
34
+
35
+ /**
36
+ * Run a vai sub-command as a child process with inherited stdio.
37
+ * @param {string[]} args
38
+ * @returns {{ status: number }}
39
+ */
40
+ function runVai(args) {
41
+ return spawnSync(process.execPath, [CLI_PATH, ...args], {
42
+ stdio: 'inherit',
43
+ env: process.env,
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Print a step header.
49
+ * @param {number} num
50
+ * @param {string} title
51
+ */
52
+ function stepHeader(num, title) {
53
+ const label = `── Step ${num}: ${title} `;
54
+ const pad = Math.max(0, 60 - label.length);
55
+ console.log(`\n${pc.bold(label)}${'─'.repeat(pad)}`);
56
+ }
57
+
58
+ /**
59
+ * Run a vai command, show it, and return whether it succeeded.
60
+ * @param {string} display - the display string (e.g. 'vai embed "hello"')
61
+ * @param {string[]} args - args to pass to vai
62
+ * @returns {boolean} success
63
+ */
64
+ function runStep(display, args) {
65
+ showCommand(display);
66
+ const result = runVai(args);
67
+ return result.status === 0;
68
+ }
69
+
70
+ /**
71
+ * Ask user whether to continue after a failure.
72
+ * @param {boolean} noPause
73
+ * @returns {Promise<boolean>} true = continue, false = abort
74
+ */
75
+ function askContinue(noPause) {
76
+ if (noPause) return Promise.resolve(true);
77
+ return new Promise((resolve) => {
78
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
79
+ rl.question(pc.yellow(' Step failed. Continue anyway? (Y/n) '), (answer) => {
80
+ rl.close();
81
+ const a = answer.trim().toLowerCase();
82
+ resolve(a === '' || a === 'y' || a === 'yes');
83
+ });
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Sleep for ms milliseconds.
89
+ * @param {number} ms
90
+ * @returns {Promise<void>}
91
+ */
92
+ function sleep(ms) {
93
+ return new Promise((resolve) => setTimeout(resolve, ms));
94
+ }
95
+
96
+ /**
97
+ * Register the demo command on a Commander program.
98
+ * @param {import('commander').Command} program
99
+ */
100
+ function registerDemo(program) {
101
+ program
102
+ .command('demo')
103
+ .description('Interactive guided walkthrough of Voyage AI features')
104
+ .option('--no-pause', 'Skip Enter prompts (for CI/recording)')
105
+ .option('--skip-pipeline', 'Skip the full pipeline step (Step 5)')
106
+ .option('--keep', 'Keep the demo collection after pipeline step')
107
+ .action(async (opts) => {
108
+ const noPause = !opts.pause;
109
+
110
+ // ── Preflight: check API key ──
111
+ const apiKey = process.env.VOYAGE_API_KEY;
112
+ if (!apiKey) {
113
+ const { getConfigValue } = require('../lib/config');
114
+ const configKey = getConfigValue('apiKey');
115
+ if (!configKey) {
116
+ console.error('');
117
+ console.error(pc.red(' ✗ VOYAGE_API_KEY is not set.'));
118
+ console.error('');
119
+ console.error(' Set it with:');
120
+ console.error(` ${pc.cyan('export VOYAGE_API_KEY="your-key"')}`);
121
+ console.error(' Or:');
122
+ console.error(` ${pc.cyan('vai config set api-key "your-key"')}`);
123
+ console.error('');
124
+ process.exit(1);
125
+ }
126
+ }
127
+
128
+ // ── Banner ──
129
+ console.log('');
130
+ console.log(pc.bold(' 🧭 Voyage AI Interactive Demo'));
131
+ console.log(pc.dim(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
132
+ console.log('');
133
+ console.log(' This walkthrough demonstrates embeddings, semantic search, and reranking');
134
+ console.log(' using Voyage AI models via MongoDB Atlas.');
135
+ console.log('');
136
+
137
+ await waitForEnter(noPause);
138
+
139
+ // ── Step 1: Ping ──
140
+ stepHeader(1, 'Check Connection');
141
+ console.log(' First, let\'s verify your API key works.');
142
+
143
+ let ok = runStep('vai ping', ['ping']);
144
+ if (!ok) {
145
+ const cont = await askContinue(noPause);
146
+ if (!cont) process.exit(1);
147
+ }
148
+
149
+ await waitForEnter(noPause);
150
+
151
+ // ── Step 2: Embeddings ──
152
+ stepHeader(2, 'Generate Embeddings');
153
+ console.log(' Embeddings convert text into numerical vectors that capture meaning.');
154
+ console.log(' Let\'s embed a sentence:');
155
+
156
+ ok = runStep('vai embed "MongoDB is the most popular document database"',
157
+ ['embed', 'MongoDB is the most popular document database']);
158
+ if (!ok) {
159
+ const cont = await askContinue(noPause);
160
+ if (!cont) process.exit(1);
161
+ }
162
+
163
+ console.log('\n Let\'s try another:');
164
+
165
+ ok = runStep('vai embed "I love using NoSQL databases for modern applications"',
166
+ ['embed', 'I love using NoSQL databases for modern applications']);
167
+ if (!ok) {
168
+ const cont = await askContinue(noPause);
169
+ if (!cont) process.exit(1);
170
+ }
171
+
172
+ console.log('');
173
+ console.log(' These two sentences are about related topics — their vectors will be');
174
+ console.log(' close together in embedding space, even though they share few words.');
175
+
176
+ await waitForEnter(noPause);
177
+
178
+ // ── Step 3: Compare Similarity ──
179
+ stepHeader(3, 'Compare Similarity');
180
+ console.log(' Let\'s embed a set of diverse documents and see how the model');
181
+ console.log(' distinguishes meaning:');
182
+
183
+ runStep('vai embed "MongoDB Atlas is a cloud database" --quiet',
184
+ ['embed', 'MongoDB Atlas is a cloud database', '--quiet']);
185
+ runStep('vai embed "The weather in Paris is lovely" --quiet',
186
+ ['embed', 'The weather in Paris is lovely', '--quiet']);
187
+ runStep('vai embed "Vector search enables AI applications" --quiet',
188
+ ['embed', 'Vector search enables AI applications', '--quiet']);
189
+
190
+ console.log('');
191
+ console.log(' Notice how the model captures semantic relationships — database topics');
192
+ console.log(' cluster together, while unrelated topics are far apart.');
193
+
194
+ await waitForEnter(noPause);
195
+
196
+ // ── Step 4: Reranking ──
197
+ stepHeader(4, 'Reranking');
198
+ console.log(' Reranking scores how relevant each document is to a specific query.');
199
+ console.log(' This is the "precision" stage of two-stage retrieval.');
200
+
201
+ ok = runStep(
202
+ 'vai rerank --query "How do I build AI search?" --documents ...',
203
+ [
204
+ 'rerank',
205
+ '--query', 'How do I build AI search?',
206
+ '--documents',
207
+ 'MongoDB Atlas provides vector search capabilities',
208
+ 'The recipe calls for two cups of flour',
209
+ 'Voyage AI embeddings power semantic retrieval',
210
+ 'Python is a popular programming language',
211
+ 'Atlas Search combines full-text and vector search',
212
+ ]
213
+ );
214
+ if (!ok) {
215
+ const cont = await askContinue(noPause);
216
+ if (!cont) process.exit(1);
217
+ }
218
+
219
+ console.log('');
220
+ console.log(' Notice how the reranker assigns HIGH scores to relevant documents');
221
+ console.log(' and LOW scores to irrelevant ones — much more decisive than');
222
+ console.log(' embedding similarity alone.');
223
+
224
+ await waitForEnter(noPause);
225
+
226
+ // ── Step 5: Full Pipeline (optional) ──
227
+ const { getConfigValue } = require('../lib/config');
228
+ const mongoUri = process.env.MONGODB_URI || getConfigValue('mongodbUri');
229
+ const skipPipeline = opts.skipPipeline || !mongoUri;
230
+
231
+ if (skipPipeline && !opts.skipPipeline) {
232
+ stepHeader(5, 'Full Pipeline (skipped)');
233
+ console.log(pc.dim(' Skipping pipeline demo — set MONGODB_URI to try the full flow.'));
234
+ console.log(pc.dim(' See: vai config set mongodb-uri "mongodb+srv://..."'));
235
+ console.log('');
236
+ } else if (!skipPipeline) {
237
+ stepHeader(5, 'Full Pipeline');
238
+ console.log(' Now let\'s put it all together: embed → store → index → search → rerank.');
239
+ console.log('');
240
+
241
+ const db = 'test';
242
+ const collection = 'demo_voyage_test';
243
+ const field = 'embedding';
244
+
245
+ const documents = [
246
+ 'MongoDB Atlas is a fully managed cloud database',
247
+ 'Voyage AI provides state of the art embedding models',
248
+ 'Vector search enables semantic retrieval for AI applications',
249
+ 'Atlas Search combines full-text and vector search capabilities',
250
+ 'The recipe calls for two cups of flour and three eggs',
251
+ ];
252
+
253
+ console.log(pc.dim(` Creating test collection: ${collection}...`));
254
+ console.log('');
255
+
256
+ let pipelineOk = true;
257
+ for (const text of documents) {
258
+ const short = text.length > 50 ? text.slice(0, 47) + '...' : text;
259
+ ok = runStep(
260
+ `vai store --db ${db} --collection ${collection} --field ${field} --text "${short}"`,
261
+ ['store', '--db', db, '--collection', collection, '--field', field, '--text', text]
262
+ );
263
+ if (!ok) {
264
+ pipelineOk = false;
265
+ const cont = await askContinue(noPause);
266
+ if (!cont) break;
267
+ }
268
+ }
269
+
270
+ if (pipelineOk) {
271
+ // Create index
272
+ ok = runStep(
273
+ `vai index create --db ${db} --collection ${collection} --field ${field} --dimensions 1024`,
274
+ ['index', 'create', '--db', db, '--collection', collection, '--field', field, '--dimensions', '1024']
275
+ );
276
+
277
+ if (ok) {
278
+ // Wait for index to be ready
279
+ console.log('');
280
+ console.log(pc.dim(' Waiting for index to build...'));
281
+
282
+ const { getConnection } = require('../lib/mongo');
283
+ let indexReady = false;
284
+ const deadline = Date.now() + 120000;
285
+
286
+ try {
287
+ const client = await getConnection();
288
+ const coll = client.db(db).collection(collection);
289
+
290
+ while (Date.now() < deadline) {
291
+ const indexes = await coll.listSearchIndexes().toArray();
292
+ const idx = indexes.find(i => i.name && i.status);
293
+ if (idx && idx.status === 'READY') {
294
+ indexReady = true;
295
+ console.log(pc.green(' ✓ Index is READY'));
296
+ break;
297
+ }
298
+ const statusStr = idx ? idx.status : 'PENDING';
299
+ process.stdout.write(pc.dim(`\r Index status: ${statusStr}...`));
300
+ await sleep(5000);
301
+ }
302
+
303
+ if (!indexReady) {
304
+ console.log(pc.yellow('\n ⚠ Index build timed out (120s). Trying search anyway...'));
305
+ }
306
+ } catch (err) {
307
+ console.log(pc.yellow(`\n ⚠ Could not check index status: ${err.message}`));
308
+ }
309
+
310
+ console.log('');
311
+
312
+ // Search
313
+ ok = runStep(
314
+ `vai search --query "cloud database for AI apps" --db ${db} --collection ${collection} --field ${field}`,
315
+ ['search', '--query', 'cloud database for AI apps', '--db', db, '--collection', collection, '--field', field]
316
+ );
317
+ }
318
+ }
319
+
320
+ // Cleanup
321
+ if (!opts.keep) {
322
+ console.log('');
323
+ console.log(pc.dim(` Cleaning up: dropping ${collection}...`));
324
+ try {
325
+ const { getConnection } = require('../lib/mongo');
326
+ const client = await getConnection();
327
+ await client.db(db).collection(collection).drop();
328
+ console.log(pc.dim(' ✓ Collection dropped.'));
329
+ } catch (err) {
330
+ console.log(pc.dim(` ⚠ Cleanup note: ${err.message}`));
331
+ }
332
+ } else {
333
+ console.log(pc.dim(` Collection ${collection} kept (--keep flag).`));
334
+ }
335
+ }
336
+
337
+ // ── Done ──
338
+ console.log('');
339
+ console.log('─'.repeat(60));
340
+ console.log(pc.bold(' 🧭 That\'s Voyage AI in action!'));
341
+ console.log('');
342
+ console.log(' Next steps:');
343
+ console.log(` • Read the docs: ${pc.cyan('https://www.mongodb.com/docs/voyageai/')}`);
344
+ console.log(` • Explore models: ${pc.cyan('vai models')}`);
345
+ console.log(` • Configure: ${pc.cyan('vai config set api-key <your-key>')}`);
346
+ console.log(` • Full pipeline: ${pc.cyan('vai store → vai index create → vai search')}`);
347
+ console.log('');
348
+ console.log(' Happy searching! 🚀');
349
+ console.log('');
350
+ });
351
+ }
352
+
353
+ module.exports = { registerDemo };
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { DEFAULT_EMBED_MODEL } = require('../lib/catalog');
3
+ const { getDefaultModel } = require('../lib/catalog');
4
4
  const { generateEmbeddings } = require('../lib/api');
5
5
  const { resolveTextInput } = require('../lib/input');
6
+ const ui = require('../lib/ui');
6
7
 
7
8
  /**
8
9
  * Register the embed command on a Commander program.
@@ -12,7 +13,7 @@ function registerEmbed(program) {
12
13
  program
13
14
  .command('embed [text]')
14
15
  .description('Generate embeddings for text')
15
- .option('-m, --model <model>', 'Embedding model', DEFAULT_EMBED_MODEL)
16
+ .option('-m, --model <model>', 'Embedding model', getDefaultModel())
16
17
  .option('-t, --input-type <type>', 'Input type: query or document')
17
18
  .option('-d, --dimensions <n>', 'Output dimensions', (v) => parseInt(v, 10))
18
19
  .option('-f, --file <path>', 'Read text from file')
@@ -23,12 +24,22 @@ function registerEmbed(program) {
23
24
  try {
24
25
  const texts = await resolveTextInput(text, opts.file);
25
26
 
27
+ const useColor = !opts.json;
28
+ const useSpinner = useColor && !opts.quiet;
29
+ let spin;
30
+ if (useSpinner) {
31
+ spin = ui.spinner('Generating embeddings...');
32
+ spin.start();
33
+ }
34
+
26
35
  const result = await generateEmbeddings(texts, {
27
36
  model: opts.model,
28
37
  inputType: opts.inputType,
29
38
  dimensions: opts.dimensions,
30
39
  });
31
40
 
41
+ if (spin) spin.stop();
42
+
32
43
  if (opts.outputFormat === 'array') {
33
44
  if (result.data.length === 1) {
34
45
  console.log(JSON.stringify(result.data[0].embedding));
@@ -45,21 +56,24 @@ function registerEmbed(program) {
45
56
 
46
57
  // Friendly output
47
58
  if (!opts.quiet) {
48
- console.log(`Model: ${result.model}`);
49
- console.log(`Texts: ${result.data.length}`);
59
+ console.log(ui.label('Model', ui.cyan(result.model)));
60
+ console.log(ui.label('Texts', String(result.data.length)));
50
61
  if (result.usage) {
51
- console.log(`Tokens: ${result.usage.total_tokens}`);
62
+ console.log(ui.label('Tokens', ui.dim(String(result.usage.total_tokens))));
52
63
  }
53
- console.log(`Dimensions: ${result.data[0]?.embedding?.length || 'N/A'}`);
64
+ console.log(ui.label('Dimensions', ui.bold(String(result.data[0]?.embedding?.length || 'N/A'))));
54
65
  console.log('');
55
66
  }
56
67
 
57
68
  for (const item of result.data) {
58
69
  const preview = item.embedding.slice(0, 5).map(v => v.toFixed(6)).join(', ');
59
- console.log(`[${item.index}] [${preview}, ...] (${item.embedding.length} dims)`);
70
+ console.log(`${ui.dim('[' + item.index + ']')} [${preview}, ...] (${item.embedding.length} dims)`);
60
71
  }
72
+
73
+ console.log('');
74
+ console.log(ui.success('Embeddings generated'));
61
75
  } catch (err) {
62
- console.error(`Error: ${err.message}`);
76
+ console.error(ui.error(err.message));
63
77
  process.exit(1);
64
78
  }
65
79
  });
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const { DEFAULT_DIMENSIONS } = require('../lib/catalog');
3
+ const { getDefaultDimensions } = require('../lib/catalog');
4
4
  const { getMongoCollection } = require('../lib/mongo');
5
+ const ui = require('../lib/ui');
5
6
 
6
7
  /**
7
8
  * Register the index command (with create, list, delete subcommands) on a Commander program.
@@ -19,7 +20,7 @@ function registerIndex(program) {
19
20
  .requiredOption('--db <database>', 'Database name')
20
21
  .requiredOption('--collection <name>', 'Collection name')
21
22
  .requiredOption('--field <name>', 'Embedding field name')
22
- .option('-d, --dimensions <n>', 'Vector dimensions', (v) => parseInt(v, 10), DEFAULT_DIMENSIONS)
23
+ .option('-d, --dimensions <n>', 'Vector dimensions', (v) => parseInt(v, 10), getDefaultDimensions())
23
24
  .option('-s, --similarity <type>', 'Similarity function: cosine, dotProduct, euclidean', 'cosine')
24
25
  .option('-n, --index-name <name>', 'Index name', 'default')
25
26
  .option('--json', 'Machine-readable JSON output')
@@ -27,6 +28,14 @@ function registerIndex(program) {
27
28
  .action(async (opts) => {
28
29
  let client;
29
30
  try {
31
+ const useColor = !opts.json;
32
+ const useSpinner = useColor && !opts.quiet;
33
+ let spin;
34
+ if (useSpinner) {
35
+ spin = ui.spinner('Creating vector search index...');
36
+ spin.start();
37
+ }
38
+
30
39
  const { client: c, collection } = await getMongoCollection(opts.db, opts.collection);
31
40
  client = c;
32
41
 
@@ -38,7 +47,7 @@ function registerIndex(program) {
38
47
  {
39
48
  type: 'vector',
40
49
  path: opts.field,
41
- numDimensions: parseInt(opts.dimensions, 10) || DEFAULT_DIMENSIONS,
50
+ numDimensions: parseInt(opts.dimensions, 10) || getDefaultDimensions(),
42
51
  similarity: opts.similarity,
43
52
  },
44
53
  ],
@@ -47,24 +56,26 @@ function registerIndex(program) {
47
56
 
48
57
  const result = await collection.createSearchIndex(indexDef);
49
58
 
59
+ if (spin) spin.stop();
60
+
50
61
  if (opts.json) {
51
62
  console.log(JSON.stringify({ indexName: result, definition: indexDef }, null, 2));
52
63
  } else if (!opts.quiet) {
53
- console.log(`✓ Vector search index created: "${result}"`);
54
- console.log(` Database: ${opts.db}`);
55
- console.log(` Collection: ${opts.collection}`);
56
- console.log(` Field: ${opts.field}`);
57
- console.log(` Dimensions: ${opts.dimensions}`);
58
- console.log(` Similarity: ${opts.similarity}`);
64
+ console.log(ui.success(`Vector search index created: "${result}"`));
65
+ console.log(ui.label('Database', opts.db));
66
+ console.log(ui.label('Collection', opts.collection));
67
+ console.log(ui.label('Field', opts.field));
68
+ console.log(ui.label('Dimensions', String(opts.dimensions)));
69
+ console.log(ui.label('Similarity', opts.similarity));
59
70
  console.log('');
60
- console.log('Note: Index may take a few minutes to become ready.');
71
+ console.log(ui.dim('Note: Index may take a few minutes to become ready.'));
61
72
  }
62
73
  } catch (err) {
63
74
  if (err.message && err.message.includes('already exists')) {
64
- console.error(`Error: Index "${opts.indexName}" already exists on ${opts.db}.${opts.collection}`);
65
- console.error('Use a different --index-name or delete the existing index first.');
75
+ console.error(ui.error(`Index "${opts.indexName}" already exists on ${opts.db}.${opts.collection}`));
76
+ console.error(ui.dim('Use a different --index-name or delete the existing index first.'));
66
77
  } else {
67
- console.error(`Error: ${err.message}`);
78
+ console.error(ui.error(err.message));
68
79
  }
69
80
  process.exit(1);
70
81
  } finally {
@@ -83,11 +94,21 @@ function registerIndex(program) {
83
94
  .action(async (opts) => {
84
95
  let client;
85
96
  try {
97
+ const useColor = !opts.json;
98
+ const useSpinner = useColor && !opts.quiet;
99
+ let spin;
100
+ if (useSpinner) {
101
+ spin = ui.spinner('Listing indexes...');
102
+ spin.start();
103
+ }
104
+
86
105
  const { client: c, collection } = await getMongoCollection(opts.db, opts.collection);
87
106
  client = c;
88
107
 
89
108
  const indexes = await collection.listSearchIndexes().toArray();
90
109
 
110
+ if (spin) spin.stop();
111
+
91
112
  if (opts.json) {
92
113
  console.log(JSON.stringify(indexes, null, 2));
93
114
  return;
@@ -99,21 +120,21 @@ function registerIndex(program) {
99
120
  }
100
121
 
101
122
  if (!opts.quiet) {
102
- console.log(`Search indexes on ${opts.db}.${opts.collection}:`);
123
+ console.log(`Search indexes on ${ui.cyan(opts.db + '.' + opts.collection)}:`);
103
124
  console.log('');
104
125
  }
105
126
 
106
127
  for (const idx of indexes) {
107
- console.log(` Name: ${idx.name}`);
108
- console.log(` Type: ${idx.type || 'N/A'}`);
109
- console.log(` Status: ${idx.status || 'N/A'}`);
128
+ console.log(ui.label('Name', ui.bold(idx.name)));
129
+ console.log(ui.label('Type', idx.type || 'N/A'));
130
+ console.log(ui.label('Status', ui.status(idx.status || 'N/A')));
110
131
  if (idx.latestDefinition) {
111
- console.log(` Fields: ${JSON.stringify(idx.latestDefinition.fields || [])}`);
132
+ console.log(ui.label('Fields', JSON.stringify(idx.latestDefinition.fields || [])));
112
133
  }
113
134
  console.log('');
114
135
  }
115
136
  } catch (err) {
116
- console.error(`Error: ${err.message}`);
137
+ console.error(ui.error(err.message));
117
138
  process.exit(1);
118
139
  } finally {
119
140
  if (client) await client.close();
@@ -132,20 +153,30 @@ function registerIndex(program) {
132
153
  .action(async (opts) => {
133
154
  let client;
134
155
  try {
156
+ const useColor = !opts.json;
157
+ const useSpinner = useColor && !opts.quiet;
158
+ let spin;
159
+ if (useSpinner) {
160
+ spin = ui.spinner('Deleting index...');
161
+ spin.start();
162
+ }
163
+
135
164
  const { client: c, collection } = await getMongoCollection(opts.db, opts.collection);
136
165
  client = c;
137
166
 
138
167
  await collection.dropSearchIndex(opts.indexName);
139
168
 
169
+ if (spin) spin.stop();
170
+
140
171
  if (opts.json) {
141
172
  console.log(JSON.stringify({ dropped: opts.indexName }, null, 2));
142
173
  } else if (!opts.quiet) {
143
- console.log(`✓ Dropped search index: "${opts.indexName}"`);
144
- console.log(` Database: ${opts.db}`);
145
- console.log(` Collection: ${opts.collection}`);
174
+ console.log(ui.success(`Dropped search index: "${opts.indexName}"`));
175
+ console.log(ui.label('Database', opts.db));
176
+ console.log(ui.label('Collection', opts.collection));
146
177
  }
147
178
  } catch (err) {
148
- console.error(`Error: ${err.message}`);
179
+ console.error(ui.error(err.message));
149
180
  process.exit(1);
150
181
  } finally {
151
182
  if (client) await client.close();
@@ -3,6 +3,7 @@
3
3
  const { MODEL_CATALOG } = require('../lib/catalog');
4
4
  const { API_BASE } = require('../lib/api');
5
5
  const { formatTable } = require('../lib/format');
6
+ const ui = require('../lib/ui');
6
7
 
7
8
  /**
8
9
  * Register the models command on a Commander program.
@@ -28,25 +29,32 @@ function registerModels(program) {
28
29
  }
29
30
 
30
31
  if (models.length === 0) {
31
- console.log(`No models found for type: ${opts.type}`);
32
+ console.log(ui.yellow(`No models found for type: ${opts.type}`));
32
33
  return;
33
34
  }
34
35
 
35
36
  if (!opts.quiet) {
36
- console.log('Voyage AI Models');
37
- console.log(`(via MongoDB AI API — ${API_BASE})`);
37
+ console.log(ui.bold('Voyage AI Models'));
38
+ console.log(ui.dim(`(via MongoDB AI API — ${API_BASE})`));
38
39
  console.log('');
39
40
  }
40
41
 
41
42
  const headers = ['Model', 'Type', 'Context', 'Dimensions', 'Price', 'Best For'];
42
- const rows = models.map(m => [m.name, m.type, m.context, m.dimensions, m.price, m.bestFor]);
43
+ const rows = models.map(m => {
44
+ const name = ui.cyan(m.name);
45
+ const type = m.type === 'embedding' ? ui.green(m.type) : ui.yellow(m.type);
46
+ const price = ui.dim(m.price);
47
+ return [name, type, m.context, m.dimensions, price, m.bestFor];
48
+ });
43
49
 
44
- console.log(formatTable(headers, rows));
50
+ // Use bold headers
51
+ const boldHeaders = headers.map(h => ui.bold(h));
52
+ console.log(formatTable(boldHeaders, rows));
45
53
 
46
54
  if (!opts.quiet) {
47
55
  console.log('');
48
- console.log('Free tier: 200M tokens (most models), 50M (domain-specific)');
49
- console.log('All 4-series models share the same embedding space.');
56
+ console.log(ui.dim('Free tier: 200M tokens (most models), 50M (domain-specific)'));
57
+ console.log(ui.dim('All 4-series models share the same embedding space.'));
50
58
  }
51
59
  });
52
60
  }