voyageai-cli 1.20.5 → 1.21.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +142 -26
  2. package/README.md +153 -2
  3. package/package.json +1 -1
  4. package/src/cli.js +12 -0
  5. package/src/commands/about.js +39 -3
  6. package/src/commands/doctor.js +325 -0
  7. package/src/commands/eval.js +420 -10
  8. package/src/commands/generate.js +220 -0
  9. package/src/commands/playground.js +93 -0
  10. package/src/commands/purge.js +271 -0
  11. package/src/commands/quickstart.js +203 -0
  12. package/src/commands/refresh.js +322 -0
  13. package/src/commands/scaffold.js +217 -0
  14. package/src/lib/codegen.js +313 -0
  15. package/src/lib/explanations.js +163 -1
  16. package/src/lib/scaffold-structure.js +114 -0
  17. package/src/lib/templates/nextjs/README.md.tpl +106 -0
  18. package/src/lib/templates/nextjs/env.example.tpl +8 -0
  19. package/src/lib/templates/nextjs/layout.jsx.tpl +29 -0
  20. package/src/lib/templates/nextjs/lib-mongo.js.tpl +111 -0
  21. package/src/lib/templates/nextjs/lib-voyage.js.tpl +103 -0
  22. package/src/lib/templates/nextjs/package.json.tpl +33 -0
  23. package/src/lib/templates/nextjs/page-search.jsx.tpl +147 -0
  24. package/src/lib/templates/nextjs/route-ingest.js.tpl +114 -0
  25. package/src/lib/templates/nextjs/route-search.js.tpl +97 -0
  26. package/src/lib/templates/nextjs/theme.js.tpl +84 -0
  27. package/src/lib/templates/python/README.md.tpl +145 -0
  28. package/src/lib/templates/python/app.py.tpl +221 -0
  29. package/src/lib/templates/python/chunker.py.tpl +127 -0
  30. package/src/lib/templates/python/env.example.tpl +12 -0
  31. package/src/lib/templates/python/mongo_client.py.tpl +125 -0
  32. package/src/lib/templates/python/requirements.txt.tpl +10 -0
  33. package/src/lib/templates/python/voyage_client.py.tpl +124 -0
  34. package/src/lib/templates/vanilla/README.md.tpl +156 -0
  35. package/src/lib/templates/vanilla/client.js.tpl +103 -0
  36. package/src/lib/templates/vanilla/connection.js.tpl +126 -0
  37. package/src/lib/templates/vanilla/env.example.tpl +11 -0
  38. package/src/lib/templates/vanilla/ingest.js.tpl +231 -0
  39. package/src/lib/templates/vanilla/package.json.tpl +31 -0
  40. package/src/lib/templates/vanilla/retrieval.js.tpl +100 -0
  41. package/src/lib/templates/vanilla/search-api.js.tpl +175 -0
  42. package/src/lib/templates/vanilla/server.js.tpl +81 -0
  43. package/src/lib/zip.js +130 -0
  44. package/src/playground/index.html +772 -33
@@ -114,6 +114,99 @@ function createPlaygroundServer() {
114
114
  return;
115
115
  }
116
116
 
117
+ // API: Generate code
118
+ if (req.method === 'POST' && req.url === '/api/generate') {
119
+ let body = '';
120
+ req.on('data', chunk => { body += chunk; });
121
+ req.on('end', () => {
122
+ try {
123
+ const { target, component, config } = JSON.parse(body);
124
+ const codegen = require('../lib/codegen');
125
+
126
+ const templateMap = {
127
+ vanilla: { client: 'client.js', connection: 'connection.js', retrieval: 'retrieval.js', ingest: 'ingest.js', 'search-api': 'search-api.js' },
128
+ nextjs: { client: 'lib-voyage.js', connection: 'lib-mongo.js', retrieval: 'route-search.js', ingest: 'route-ingest.js', 'search-page': 'page-search.jsx' },
129
+ python: { client: 'voyage_client.py', connection: 'mongo_client.py', retrieval: 'app.py', ingest: 'chunker.py' },
130
+ };
131
+
132
+ const templateName = (templateMap[target] || {})[component];
133
+ if (!templateName) {
134
+ res.writeHead(400, { 'Content-Type': 'application/json' });
135
+ res.end(JSON.stringify({ error: `Unknown component: ${component}` }));
136
+ return;
137
+ }
138
+
139
+ const context = codegen.buildContext(config || {}, { projectName: 'my-app' });
140
+ const code = codegen.renderTemplate(target, templateName.replace(/\.(js|jsx|py)$/, ''), context);
141
+
142
+ res.writeHead(200, { 'Content-Type': 'application/json' });
143
+ res.end(JSON.stringify({ code, filename: templateName }));
144
+ } catch (err) {
145
+ res.writeHead(500, { 'Content-Type': 'application/json' });
146
+ res.end(JSON.stringify({ error: err.message }));
147
+ }
148
+ });
149
+ return;
150
+ }
151
+
152
+ // API: Scaffold project (returns ZIP for web mode)
153
+ if (req.method === 'POST' && req.url === '/api/scaffold') {
154
+ let body = '';
155
+ req.on('data', chunk => { body += chunk; });
156
+ req.on('end', () => {
157
+ try {
158
+ const { projectName, target, config } = JSON.parse(body);
159
+ const codegen = require('../lib/codegen');
160
+ const { PROJECT_STRUCTURE } = require('../lib/scaffold-structure');
161
+ const { createZip } = require('../lib/zip');
162
+
163
+ const structure = PROJECT_STRUCTURE[target];
164
+ if (!structure) {
165
+ res.writeHead(400, { 'Content-Type': 'application/json' });
166
+ res.end(JSON.stringify({ error: `Unknown target: ${target}` }));
167
+ return;
168
+ }
169
+
170
+ const context = codegen.buildContext(config || {}, { projectName: projectName || 'my-app' });
171
+ const files = [];
172
+
173
+ // Render template files
174
+ for (const file of structure.files) {
175
+ const content = codegen.renderTemplate(target, file.template.replace(/\.(js|jsx|py|json|md|txt)$/, ''), context);
176
+ files.push({
177
+ name: `${projectName}/${file.output}`,
178
+ content,
179
+ });
180
+ }
181
+
182
+ // Add extra static files
183
+ if (structure.extraFiles) {
184
+ for (const file of structure.extraFiles) {
185
+ const content = typeof file.content === 'function' ? file.content(context) : file.content;
186
+ files.push({
187
+ name: `${projectName}/${file.output}`,
188
+ content,
189
+ });
190
+ }
191
+ }
192
+
193
+ // Create ZIP
194
+ const zipBuffer = createZip(files);
195
+
196
+ res.writeHead(200, {
197
+ 'Content-Type': 'application/zip',
198
+ 'Content-Disposition': `attachment; filename="${projectName}.zip"`,
199
+ 'Content-Length': zipBuffer.length,
200
+ });
201
+ res.end(zipBuffer);
202
+ } catch (err) {
203
+ res.writeHead(500, { 'Content-Type': 'application/json' });
204
+ res.end(JSON.stringify({ error: err.message }));
205
+ }
206
+ });
207
+ return;
208
+ }
209
+
117
210
  // API: Concepts (from vai explain)
118
211
  if (req.method === 'GET' && req.url === '/api/concepts') {
119
212
  const { concepts } = require('../lib/explanations');
@@ -0,0 +1,271 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const p = require('@clack/prompts');
6
+ const { loadProject } = require('../lib/project');
7
+ const { connect, close } = require('../lib/mongo');
8
+ const ui = require('../lib/ui');
9
+
10
+ /**
11
+ * Build a MongoDB filter from the provided criteria.
12
+ */
13
+ function buildFilter(options) {
14
+ const conditions = [];
15
+
16
+ // Filter by source pattern (glob-like)
17
+ if (options.source) {
18
+ // Convert glob pattern to regex
19
+ const pattern = options.source
20
+ .replace(/\./g, '\\.')
21
+ .replace(/\*/g, '.*')
22
+ .replace(/\?/g, '.');
23
+ conditions.push({ 'metadata.source': { $regex: pattern } });
24
+ }
25
+
26
+ // Filter by embedded date
27
+ if (options.before) {
28
+ const date = new Date(options.before);
29
+ if (isNaN(date.getTime())) {
30
+ throw new Error(`Invalid date format: ${options.before}`);
31
+ }
32
+ conditions.push({ _embeddedAt: { $lt: date } });
33
+ }
34
+
35
+ // Filter by model
36
+ if (options.model) {
37
+ conditions.push({ _model: options.model });
38
+ }
39
+
40
+ // Raw MongoDB filter
41
+ if (options.filter) {
42
+ try {
43
+ const rawFilter = JSON.parse(options.filter);
44
+ conditions.push(rawFilter);
45
+ } catch (err) {
46
+ throw new Error(`Invalid JSON filter: ${err.message}`);
47
+ }
48
+ }
49
+
50
+ // Combine conditions with $and
51
+ if (conditions.length === 0) {
52
+ return {};
53
+ } else if (conditions.length === 1) {
54
+ return conditions[0];
55
+ } else {
56
+ return { $and: conditions };
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Check which documents have stale source files (file no longer exists on disk).
62
+ */
63
+ async function findStaleDocuments(collection, baseDir) {
64
+ const docs = await collection.find({ 'metadata.source': { $exists: true } }).toArray();
65
+ const staleIds = [];
66
+
67
+ for (const doc of docs) {
68
+ const source = doc.metadata?.source;
69
+ if (source) {
70
+ // Resolve relative to baseDir or treat as absolute
71
+ const filePath = path.isAbsolute(source) ? source : path.join(baseDir, source);
72
+ if (!fs.existsSync(filePath)) {
73
+ staleIds.push(doc._id);
74
+ }
75
+ }
76
+ }
77
+
78
+ return staleIds;
79
+ }
80
+
81
+ /**
82
+ * Format a sample of documents for display.
83
+ */
84
+ function formatSample(docs, limit = 5) {
85
+ const sample = docs.slice(0, limit);
86
+ return sample.map(doc => {
87
+ const source = doc.metadata?.source || doc._id?.toString() || 'unknown';
88
+ const model = doc._model || 'unknown';
89
+ const date = doc._embeddedAt ? new Date(doc._embeddedAt).toISOString().split('T')[0] : 'unknown';
90
+ return ` • ${source} (model: ${model}, date: ${date})`;
91
+ }).join('\n');
92
+ }
93
+
94
+ /**
95
+ * Execute the purge command.
96
+ */
97
+ async function purge(options = {}) {
98
+ const quiet = options.quiet || options.json;
99
+
100
+ // Load project config
101
+ const project = loadProject();
102
+ const db = options.db || project.db || process.env.VAI_DB || 'vai';
103
+ const collectionName = options.collection || project.collection || process.env.VAI_COLLECTION || 'embeddings';
104
+
105
+ if (!quiet) {
106
+ p.intro(ui.title('vai purge'));
107
+ }
108
+
109
+ // Validate that at least one filter is provided
110
+ if (!options.source && !options.before && !options.model && !options.filter && !options.stale) {
111
+ if (options.json) {
112
+ console.log(JSON.stringify({ error: 'No filter criteria provided. Use --source, --before, --model, --filter, or --stale.' }));
113
+ } else {
114
+ p.log.error('No filter criteria provided.');
115
+ p.log.info('Use --source, --before, --model, --filter, or --stale to specify what to purge.');
116
+ }
117
+ return { success: false, error: 'No filter criteria' };
118
+ }
119
+
120
+ let client;
121
+ try {
122
+ // Connect to MongoDB
123
+ if (!quiet) {
124
+ p.log.step(`Connecting to database: ${db}`);
125
+ }
126
+ client = await connect(db);
127
+ const collection = client.db(db).collection(collectionName);
128
+
129
+ let filter = {};
130
+ let staleIds = [];
131
+
132
+ if (options.stale) {
133
+ // Find documents with stale source files
134
+ if (!quiet) {
135
+ p.log.step('Scanning for stale documents (source files that no longer exist)...');
136
+ }
137
+ const baseDir = project.root || process.cwd();
138
+ staleIds = await findStaleDocuments(collection, baseDir);
139
+
140
+ if (staleIds.length === 0) {
141
+ if (options.json) {
142
+ console.log(JSON.stringify({ success: true, count: 0, message: 'No stale documents found' }));
143
+ } else {
144
+ p.log.success('No stale documents found.');
145
+ p.outro('Nothing to purge.');
146
+ }
147
+ return { success: true, count: 0 };
148
+ }
149
+
150
+ filter = { _id: { $in: staleIds } };
151
+ } else {
152
+ // Build filter from criteria
153
+ filter = buildFilter(options);
154
+ }
155
+
156
+ // Count matching documents
157
+ const count = options.stale ? staleIds.length : await collection.countDocuments(filter);
158
+
159
+ if (count === 0) {
160
+ if (options.json) {
161
+ console.log(JSON.stringify({ success: true, count: 0, message: 'No matching documents found' }));
162
+ } else {
163
+ p.log.success('No matching documents found.');
164
+ p.outro('Nothing to purge.');
165
+ }
166
+ return { success: true, count: 0 };
167
+ }
168
+
169
+ // Get sample for display
170
+ const sampleDocs = await collection.find(filter).limit(5).toArray();
171
+
172
+ if (options.json) {
173
+ if (options.dryRun) {
174
+ console.log(JSON.stringify({
175
+ dryRun: true,
176
+ count,
177
+ sample: sampleDocs.map(d => ({
178
+ id: d._id?.toString(),
179
+ source: d.metadata?.source,
180
+ model: d._model,
181
+ embeddedAt: d._embeddedAt,
182
+ })),
183
+ }));
184
+ return { success: true, dryRun: true, count };
185
+ }
186
+ } else {
187
+ // Show what will be deleted
188
+ p.log.warn(`Found ${count} document${count === 1 ? '' : 's'} matching criteria:`);
189
+ console.log(formatSample(sampleDocs));
190
+ if (count > 5) {
191
+ console.log(` ... and ${count - 5} more`);
192
+ }
193
+ console.log();
194
+ }
195
+
196
+ // Dry run - stop here
197
+ if (options.dryRun) {
198
+ if (!quiet) {
199
+ p.log.info('Dry run - no documents deleted.');
200
+ p.outro(`Would delete ${count} document${count === 1 ? '' : 's'}.`);
201
+ }
202
+ return { success: true, dryRun: true, count };
203
+ }
204
+
205
+ // Confirm unless --force
206
+ if (!options.force && !options.json) {
207
+ const confirmed = await p.confirm({
208
+ message: `Delete ${count} document${count === 1 ? '' : 's'}? This cannot be undone.`,
209
+ initialValue: false,
210
+ });
211
+
212
+ if (p.isCancel(confirmed) || !confirmed) {
213
+ p.log.info('Purge cancelled.');
214
+ p.outro('No documents deleted.');
215
+ return { success: false, cancelled: true };
216
+ }
217
+ }
218
+
219
+ // Delete documents
220
+ if (!quiet) {
221
+ p.log.step('Deleting documents...');
222
+ }
223
+
224
+ const result = await collection.deleteMany(filter);
225
+ const deleted = result.deletedCount;
226
+
227
+ if (options.json) {
228
+ console.log(JSON.stringify({ success: true, deleted }));
229
+ } else {
230
+ p.log.success(`Deleted ${deleted} document${deleted === 1 ? '' : 's'}.`);
231
+ p.outro('Purge complete.');
232
+ }
233
+
234
+ return { success: true, deleted };
235
+
236
+ } catch (err) {
237
+ if (options.json) {
238
+ console.log(JSON.stringify({ error: err.message }));
239
+ } else {
240
+ p.log.error(`Purge failed: ${err.message}`);
241
+ }
242
+ return { success: false, error: err.message };
243
+ } finally {
244
+ if (client) {
245
+ await close();
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Register the purge command with Commander.
252
+ */
253
+ function register(program) {
254
+ program
255
+ .command('purge')
256
+ .description('Remove embeddings from MongoDB based on criteria')
257
+ .option('--db <database>', 'Database name')
258
+ .option('--collection <name>', 'Collection name')
259
+ .option('--source <glob>', 'Filter by metadata.source pattern')
260
+ .option('--before <date>', 'Filter by _embeddedAt before date (ISO 8601)')
261
+ .option('-m, --model <model>', 'Filter by _model field')
262
+ .option('--filter <json>', 'Raw MongoDB filter (JSON)')
263
+ .option('--stale', 'Remove docs whose source files no longer exist')
264
+ .option('--force', 'Skip confirmation prompt')
265
+ .option('--dry-run', 'Show what would be deleted without acting')
266
+ .option('--json', 'Machine-readable output')
267
+ .option('-q, --quiet', 'Suppress non-essential output')
268
+ .action(purge);
269
+ }
270
+
271
+ module.exports = { register, purge, buildFilter };
@@ -0,0 +1,203 @@
1
+ 'use strict';
2
+
3
+ const pc = require('picocolors');
4
+ const readline = require('readline');
5
+ const { getConfigValue } = require('../lib/config');
6
+ const { embed } = require('../lib/api');
7
+
8
+ /**
9
+ * vai quickstart — Zero-to-search interactive tutorial
10
+ * Gets developers from nothing to their first semantic search in minutes.
11
+ */
12
+
13
+ const SAMPLE_DOCS = [
14
+ "MongoDB Atlas Vector Search enables semantic search on your data using machine learning embeddings.",
15
+ "Voyage AI provides state-of-the-art embedding models with the best quality-to-cost ratio.",
16
+ "RAG (Retrieval-Augmented Generation) combines vector search with LLMs for accurate AI responses.",
17
+ "The shared embedding space in Voyage 4 models lets you embed queries and documents with different models.",
18
+ "Reranking improves search precision by re-scoring results with a cross-encoder model.",
19
+ ];
20
+
21
+ function sleep(ms) {
22
+ return new Promise(resolve => setTimeout(resolve, ms));
23
+ }
24
+
25
+ function createPrompt() {
26
+ const rl = readline.createInterface({
27
+ input: process.stdin,
28
+ output: process.stdout,
29
+ });
30
+
31
+ return {
32
+ ask: (question) => new Promise((resolve) => {
33
+ rl.question(question, (answer) => resolve(answer));
34
+ }),
35
+ close: () => rl.close(),
36
+ };
37
+ }
38
+
39
+ async function runQuickstart(options = {}) {
40
+ const { skip } = options;
41
+
42
+ console.log(pc.bold('\n🚀 Voyage AI CLI Quickstart\n'));
43
+ console.log(pc.dim('This tutorial will get you from zero to semantic search in 2 minutes.\n'));
44
+
45
+ // Check for API key
46
+ const apiKey = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
47
+ if (!apiKey) {
48
+ console.log(pc.red('✗ No API key found.\n'));
49
+ console.log(' First, get a free API key:');
50
+ console.log(pc.cyan(' → https://dash.voyageai.com/api-keys\n'));
51
+ console.log(' Then configure it:');
52
+ console.log(pc.cyan(' → vai config set api-key YOUR_KEY\n'));
53
+ console.log(' Or set the environment variable:');
54
+ console.log(pc.cyan(' → export VOYAGE_API_KEY=YOUR_KEY\n'));
55
+ return 1;
56
+ }
57
+
58
+ console.log(pc.green('✓') + ' API key configured\n');
59
+
60
+ // Step 1: Explain what we're doing
61
+ console.log(pc.bold('Step 1: Understanding Embeddings'));
62
+ console.log(pc.dim('─'.repeat(40)));
63
+ console.log(`
64
+ Embeddings turn text into ${pc.cyan('vectors')} (arrays of numbers) that capture
65
+ meaning. Similar texts have similar vectors, enabling semantic search.
66
+
67
+ We'll embed these ${SAMPLE_DOCS.length} sample documents:
68
+ `);
69
+
70
+ SAMPLE_DOCS.forEach((doc, i) => {
71
+ const preview = doc.length > 70 ? doc.slice(0, 70) + '...' : doc;
72
+ console.log(` ${i + 1}. ${pc.dim(preview)}`);
73
+ });
74
+ console.log('');
75
+
76
+ if (!skip) {
77
+ const prompt = createPrompt();
78
+ await prompt.ask(pc.dim('Press Enter to continue...'));
79
+ prompt.close();
80
+ }
81
+
82
+ // Step 2: Embed the documents
83
+ console.log(pc.bold('\nStep 2: Embedding Documents'));
84
+ console.log(pc.dim('─'.repeat(40)));
85
+ console.log(`
86
+ Running: ${pc.cyan('vai embed --model voyage-3-lite')}
87
+ `);
88
+
89
+ let embeddings;
90
+ try {
91
+ process.stdout.write(' Embedding documents... ');
92
+ const result = await embed({
93
+ texts: SAMPLE_DOCS,
94
+ model: 'voyage-3-lite',
95
+ inputType: 'document',
96
+ });
97
+ embeddings = result.embeddings;
98
+ console.log(pc.green('✓'));
99
+ console.log(`
100
+ ${pc.green('✓')} Created ${embeddings.length} embeddings
101
+ ${pc.dim(` Dimensions: ${embeddings[0].length}`)}
102
+ ${pc.dim(` Model: voyage-3-lite`)}
103
+ `);
104
+ } catch (err) {
105
+ console.log(pc.red('✗'));
106
+ console.log(pc.red(`\n Error: ${err.message}\n`));
107
+ console.log(' Check your API key with: vai doctor\n');
108
+ return 1;
109
+ }
110
+
111
+ // Step 3: Search
112
+ console.log(pc.bold('Step 3: Semantic Search'));
113
+ console.log(pc.dim('─'.repeat(40)));
114
+
115
+ const query = 'How do I improve search accuracy?';
116
+ console.log(`
117
+ Now let's search! We'll embed a query and find the most similar documents.
118
+
119
+ Query: "${pc.cyan(query)}"
120
+ `);
121
+
122
+ try {
123
+ process.stdout.write(' Embedding query... ');
124
+ const queryResult = await embed({
125
+ texts: [query],
126
+ model: 'voyage-3-lite',
127
+ inputType: 'query',
128
+ });
129
+ const queryEmbedding = queryResult.embeddings[0];
130
+ console.log(pc.green('✓'));
131
+
132
+ // Calculate similarities
133
+ const similarities = embeddings.map((docEmb, i) => {
134
+ const dotProduct = docEmb.reduce((sum, val, j) => sum + val * queryEmbedding[j], 0);
135
+ const normA = Math.sqrt(docEmb.reduce((sum, val) => sum + val * val, 0));
136
+ const normB = Math.sqrt(queryEmbedding.reduce((sum, val) => sum + val * val, 0));
137
+ return {
138
+ index: i,
139
+ score: dotProduct / (normA * normB),
140
+ text: SAMPLE_DOCS[i],
141
+ };
142
+ });
143
+
144
+ // Sort by similarity
145
+ similarities.sort((a, b) => b.score - a.score);
146
+
147
+ console.log(`
148
+ ${pc.bold('Results (ranked by similarity):')}
149
+ `);
150
+
151
+ similarities.forEach((item, rank) => {
152
+ const scoreColor = item.score > 0.5 ? pc.green : item.score > 0.3 ? pc.yellow : pc.dim;
153
+ const preview = item.text.length > 60 ? item.text.slice(0, 60) + '...' : item.text;
154
+ console.log(` ${rank + 1}. ${scoreColor(item.score.toFixed(3))} ${preview}`);
155
+ });
156
+
157
+ } catch (err) {
158
+ console.log(pc.red('✗'));
159
+ console.log(pc.red(`\n Error: ${err.message}\n`));
160
+ return 1;
161
+ }
162
+
163
+ // Success!
164
+ console.log(pc.bold('\n✨ Congratulations!'));
165
+ console.log(pc.dim('─'.repeat(40)));
166
+ console.log(`
167
+ You just performed your first semantic search with Voyage AI!
168
+
169
+ The top result about ${pc.cyan('reranking')} is relevant because it discusses
170
+ improving search ${pc.cyan('precision')} — even though it doesn't contain the
171
+ exact words "improve" or "accuracy". That's the power of embeddings!
172
+
173
+ ${pc.bold('Why Voyage AI?')}
174
+ • ${pc.cyan('Best quality-to-cost ratio')} — SOTA quality at lower prices
175
+ • ${pc.cyan('Shared embedding space')} — mix models for cost optimization
176
+ • ${pc.cyan('Domain-specific models')} — code, finance, law, multilingual
177
+ • ${pc.cyan('Reranking')} — boost precision with two-stage retrieval
178
+
179
+ ${pc.bold('Next Steps:')}
180
+ ${pc.cyan('vai explain embeddings')} — Learn more about how embeddings work
181
+ ${pc.cyan('vai explain reranking')} — Understand two-stage retrieval
182
+ ${pc.cyan('vai demo')} — Full interactive walkthrough
183
+ ${pc.cyan('vai pipeline')} — Build a complete RAG pipeline
184
+ ${pc.cyan('vai playground')} — Visual exploration in your browser
185
+
186
+ ${pc.dim('Docs: https://docs.voyageai.com | Dashboard: https://dash.voyageai.com')}
187
+ `);
188
+
189
+ return 0;
190
+ }
191
+
192
+ function register(program) {
193
+ program
194
+ .command('quickstart')
195
+ .description('Zero-to-search tutorial — learn semantic search in 2 minutes')
196
+ .option('--skip', 'Skip interactive prompts')
197
+ .action(async (options) => {
198
+ const exitCode = await runQuickstart(options);
199
+ process.exit(exitCode);
200
+ });
201
+ }
202
+
203
+ module.exports = { register, runQuickstart };