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.
- package/.github/workflows/ci.yml +21 -0
- package/CHANGELOG.md +50 -0
- package/CONTRIBUTING.md +81 -0
- package/README.md +51 -2
- package/package.json +9 -2
- package/src/cli.js +18 -1
- package/src/commands/config.js +157 -0
- package/src/commands/demo.js +353 -0
- package/src/commands/embed.js +22 -8
- package/src/commands/index.js +54 -23
- package/src/commands/models.js +15 -7
- package/src/commands/ping.js +169 -0
- package/src/commands/rerank.js +21 -7
- package/src/commands/search.js +23 -10
- package/src/commands/store.js +45 -27
- package/src/lib/api.js +9 -5
- package/src/lib/banner.js +55 -0
- package/src/lib/catalog.js +20 -0
- package/src/lib/config.js +107 -0
- package/src/lib/mongo.js +6 -4
- package/src/lib/ui.js +47 -0
- package/test/commands/config.test.js +35 -0
- package/test/commands/demo.test.js +46 -0
- package/test/commands/models.test.js +89 -0
- package/test/commands/ping.test.js +155 -0
- package/test/lib/api.test.js +125 -0
- package/test/lib/banner.test.js +44 -0
- package/test/lib/catalog.test.js +67 -0
- package/test/lib/config.test.js +124 -0
- package/test/lib/format.test.js +75 -0
- package/test/lib/input.test.js +48 -0
- package/test/lib/ui.test.js +79 -0
- package/voyageai-cli-1.1.0.tgz +0 -0
|
@@ -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 };
|
package/src/commands/embed.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
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',
|
|
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(
|
|
49
|
-
console.log(
|
|
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(
|
|
62
|
+
console.log(ui.label('Tokens', ui.dim(String(result.usage.total_tokens))));
|
|
52
63
|
}
|
|
53
|
-
console.log(
|
|
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(
|
|
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(
|
|
76
|
+
console.error(ui.error(err.message));
|
|
63
77
|
process.exit(1);
|
|
64
78
|
}
|
|
65
79
|
});
|
package/src/commands/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
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),
|
|
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) ||
|
|
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(
|
|
54
|
-
console.log(
|
|
55
|
-
console.log(
|
|
56
|
-
console.log(
|
|
57
|
-
console.log(
|
|
58
|
-
console.log(
|
|
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(`
|
|
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(
|
|
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
|
|
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(
|
|
108
|
-
console.log(
|
|
109
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
144
|
-
console.log(
|
|
145
|
-
console.log(
|
|
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(
|
|
179
|
+
console.error(ui.error(err.message));
|
|
149
180
|
process.exit(1);
|
|
150
181
|
} finally {
|
|
151
182
|
if (client) await client.close();
|
package/src/commands/models.js
CHANGED
|
@@ -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 =>
|
|
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
|
-
|
|
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
|
}
|