voyageai-cli 1.1.0 → 1.3.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,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { API_BASE, requireApiKey } = require('../lib/api');
|
|
4
|
+
const ui = require('../lib/ui');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register the ping command on a Commander program.
|
|
8
|
+
* @param {import('commander').Command} program
|
|
9
|
+
*/
|
|
10
|
+
function registerPing(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('ping')
|
|
13
|
+
.description('Test connectivity to Voyage AI API (and optionally MongoDB)')
|
|
14
|
+
.option('--json', 'Machine-readable JSON output')
|
|
15
|
+
.option('-q, --quiet', 'Suppress non-essential output')
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
const results = {};
|
|
18
|
+
|
|
19
|
+
// ── Voyage AI ping ──
|
|
20
|
+
let apiKey;
|
|
21
|
+
try {
|
|
22
|
+
apiKey = requireApiKey();
|
|
23
|
+
} catch {
|
|
24
|
+
// requireApiKey calls process.exit, but just in case
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const useColor = !opts.json;
|
|
29
|
+
const useSpinner = useColor && !opts.quiet;
|
|
30
|
+
|
|
31
|
+
const model = 'voyage-4-lite';
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
|
|
34
|
+
let spin;
|
|
35
|
+
if (useSpinner) {
|
|
36
|
+
spin = ui.spinner('Testing Voyage AI connection...');
|
|
37
|
+
spin.start();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(`${API_BASE}/embeddings`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
input: ['ping'],
|
|
49
|
+
model,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const elapsed = Date.now() - startTime;
|
|
54
|
+
|
|
55
|
+
if (response.status === 401 || response.status === 403) {
|
|
56
|
+
if (spin) spin.stop();
|
|
57
|
+
results.voyage = { ok: false, error: 'auth', elapsed };
|
|
58
|
+
if (opts.json) {
|
|
59
|
+
console.log(JSON.stringify({ ok: false, error: 'Authentication failed', elapsed }));
|
|
60
|
+
} else {
|
|
61
|
+
console.error(ui.error(`Authentication failed (${response.status})`));
|
|
62
|
+
console.error('');
|
|
63
|
+
console.error('Your API key may be invalid or expired.');
|
|
64
|
+
console.error('Get a new key: MongoDB Atlas → AI Models → Create model API key');
|
|
65
|
+
console.error('Then: export VOYAGE_API_KEY="your-new-key"');
|
|
66
|
+
}
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
if (spin) spin.stop();
|
|
72
|
+
const body = await response.text();
|
|
73
|
+
results.voyage = { ok: false, error: `HTTP ${response.status}`, elapsed };
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
console.log(JSON.stringify({ ok: false, error: `API error (${response.status})`, detail: body, elapsed }));
|
|
76
|
+
} else {
|
|
77
|
+
console.error(ui.error(`API error (${response.status}): ${body}`));
|
|
78
|
+
}
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
const dims = data.data && data.data[0] ? data.data[0].embedding.length : 'unknown';
|
|
84
|
+
const tokens = data.usage ? data.usage.total_tokens : 'unknown';
|
|
85
|
+
|
|
86
|
+
results.voyage = { ok: true, elapsed, model, dimensions: dims, tokens, endpoint: API_BASE };
|
|
87
|
+
|
|
88
|
+
if (spin) spin.stop();
|
|
89
|
+
|
|
90
|
+
if (opts.json) {
|
|
91
|
+
// JSON output is emitted at the end after MongoDB check
|
|
92
|
+
} else if (opts.quiet) {
|
|
93
|
+
console.log(`ok ${elapsed}ms`);
|
|
94
|
+
} else {
|
|
95
|
+
console.log(ui.success(`Connected to Voyage AI API ${ui.dim('(' + elapsed + 'ms)')}`));
|
|
96
|
+
console.log(ui.label('Endpoint', API_BASE));
|
|
97
|
+
console.log(ui.label('Model', model));
|
|
98
|
+
console.log(ui.label('Dimensions', String(dims)));
|
|
99
|
+
console.log(ui.label('Tokens', String(tokens)));
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (spin) spin.stop();
|
|
103
|
+
const elapsed = Date.now() - startTime;
|
|
104
|
+
results.voyage = { ok: false, error: 'network', elapsed };
|
|
105
|
+
if (opts.json) {
|
|
106
|
+
console.log(JSON.stringify({ ok: false, error: 'Network error', detail: err.message, elapsed }));
|
|
107
|
+
} else {
|
|
108
|
+
console.error(ui.error(`Connection failed: ${err.message}`));
|
|
109
|
+
console.error('');
|
|
110
|
+
console.error('Check your internet connection and try again.');
|
|
111
|
+
}
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── MongoDB ping (optional) ──
|
|
116
|
+
const { getConfigValue } = require('../lib/config');
|
|
117
|
+
const mongoUri = process.env.MONGODB_URI || getConfigValue('mongodbUri');
|
|
118
|
+
if (mongoUri) {
|
|
119
|
+
const mongoStart = Date.now();
|
|
120
|
+
let mongoSpin;
|
|
121
|
+
if (useSpinner) {
|
|
122
|
+
mongoSpin = ui.spinner('Testing MongoDB connection...');
|
|
123
|
+
mongoSpin.start();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const { MongoClient } = require('mongodb');
|
|
128
|
+
const client = new MongoClient(mongoUri);
|
|
129
|
+
await client.connect();
|
|
130
|
+
await client.db('admin').command({ ping: 1 });
|
|
131
|
+
const mongoElapsed = Date.now() - mongoStart;
|
|
132
|
+
|
|
133
|
+
// Extract cluster hostname from URI
|
|
134
|
+
let cluster = 'unknown';
|
|
135
|
+
try {
|
|
136
|
+
const match = mongoUri.match(/@([^/?]+)/);
|
|
137
|
+
if (match) cluster = match[1];
|
|
138
|
+
} catch { /* ignore */ }
|
|
139
|
+
|
|
140
|
+
results.mongodb = { ok: true, elapsed: mongoElapsed, cluster };
|
|
141
|
+
|
|
142
|
+
if (mongoSpin) mongoSpin.stop();
|
|
143
|
+
|
|
144
|
+
if (!opts.json && !opts.quiet) {
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(ui.success(`Connected to MongoDB Atlas ${ui.dim('(' + mongoElapsed + 'ms)')}`));
|
|
147
|
+
console.log(ui.label('Cluster', cluster));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await client.close();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (mongoSpin) mongoSpin.stop();
|
|
153
|
+
const mongoElapsed = Date.now() - mongoStart;
|
|
154
|
+
results.mongodb = { ok: false, elapsed: mongoElapsed, error: err.message };
|
|
155
|
+
if (!opts.json && !opts.quiet) {
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(ui.error(`MongoDB connection failed ${ui.dim('(' + mongoElapsed + 'ms)')}: ${err.message}`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Emit JSON at the end with all results
|
|
163
|
+
if (opts.json) {
|
|
164
|
+
console.log(JSON.stringify({ ok: true, ...results }, null, 2));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { registerPing };
|
package/src/commands/rerank.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const { DEFAULT_RERANK_MODEL } = require('../lib/catalog');
|
|
5
5
|
const { apiRequest } = require('../lib/api');
|
|
6
|
+
const ui = require('../lib/ui');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Register the rerank command on a Commander program.
|
|
@@ -63,7 +64,7 @@ function registerRerank(program) {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
if (!documents || documents.length === 0) {
|
|
66
|
-
console.error('
|
|
67
|
+
console.error(ui.error('No documents provided. Use --documents, --documents-file, or pipe via stdin.'));
|
|
67
68
|
process.exit(1);
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -76,19 +77,29 @@ function registerRerank(program) {
|
|
|
76
77
|
body.top_k = opts.topK;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
const useColor = !opts.json;
|
|
81
|
+
const useSpinner = useColor && !opts.quiet;
|
|
82
|
+
let spin;
|
|
83
|
+
if (useSpinner) {
|
|
84
|
+
spin = ui.spinner('Reranking documents...');
|
|
85
|
+
spin.start();
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
const result = await apiRequest('/rerank', body);
|
|
80
89
|
|
|
90
|
+
if (spin) spin.stop();
|
|
91
|
+
|
|
81
92
|
if (opts.json) {
|
|
82
93
|
console.log(JSON.stringify(result, null, 2));
|
|
83
94
|
return;
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
if (!opts.quiet) {
|
|
87
|
-
console.log(
|
|
88
|
-
console.log(
|
|
89
|
-
console.log(
|
|
98
|
+
console.log(ui.label('Model', ui.cyan(result.model)));
|
|
99
|
+
console.log(ui.label('Query', ui.cyan(`"${opts.query}"`)));
|
|
100
|
+
console.log(ui.label('Results', String(result.data?.length || 0)));
|
|
90
101
|
if (result.usage) {
|
|
91
|
-
console.log(
|
|
102
|
+
console.log(ui.label('Tokens', ui.dim(String(result.usage.total_tokens))));
|
|
92
103
|
}
|
|
93
104
|
console.log('');
|
|
94
105
|
}
|
|
@@ -97,11 +108,14 @@ function registerRerank(program) {
|
|
|
97
108
|
for (const item of result.data) {
|
|
98
109
|
const docPreview = documents[item.index].substring(0, 80);
|
|
99
110
|
const ellipsis = documents[item.index].length > 80 ? '...' : '';
|
|
100
|
-
console.log(
|
|
111
|
+
console.log(`${ui.dim('[' + item.index + ']')} Score: ${ui.score(item.relevance_score)} ${ui.dim('"' + docPreview + ellipsis + '"')}`);
|
|
101
112
|
}
|
|
102
113
|
}
|
|
114
|
+
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(ui.success('Reranking complete'));
|
|
103
117
|
} catch (err) {
|
|
104
|
-
console.error(
|
|
118
|
+
console.error(ui.error(err.message));
|
|
105
119
|
process.exit(1);
|
|
106
120
|
}
|
|
107
121
|
});
|
package/src/commands/search.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 { getMongoCollection } = require('../lib/mongo');
|
|
6
|
+
const ui = require('../lib/ui');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Register the search command on a Commander program.
|
|
@@ -17,7 +18,7 @@ function registerSearch(program) {
|
|
|
17
18
|
.requiredOption('--collection <name>', 'Collection name')
|
|
18
19
|
.requiredOption('--index <name>', 'Vector search index name')
|
|
19
20
|
.requiredOption('--field <name>', 'Embedding field name')
|
|
20
|
-
.option('-m, --model <model>', 'Embedding model',
|
|
21
|
+
.option('-m, --model <model>', 'Embedding model', getDefaultModel())
|
|
21
22
|
.option('--input-type <type>', 'Input type for query embedding', 'query')
|
|
22
23
|
.option('-d, --dimensions <n>', 'Output dimensions', (v) => parseInt(v, 10))
|
|
23
24
|
.option('-l, --limit <n>', 'Maximum results', (v) => parseInt(v, 10), 10)
|
|
@@ -29,6 +30,14 @@ function registerSearch(program) {
|
|
|
29
30
|
.action(async (opts) => {
|
|
30
31
|
let client;
|
|
31
32
|
try {
|
|
33
|
+
const useColor = !opts.json;
|
|
34
|
+
const useSpinner = useColor && !opts.quiet;
|
|
35
|
+
let spin;
|
|
36
|
+
if (useSpinner) {
|
|
37
|
+
spin = ui.spinner('Searching...');
|
|
38
|
+
spin.start();
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
const embedResult = await generateEmbeddings([opts.query], {
|
|
33
42
|
model: opts.model,
|
|
34
43
|
inputType: opts.inputType,
|
|
@@ -54,7 +63,8 @@ function registerSearch(program) {
|
|
|
54
63
|
try {
|
|
55
64
|
vectorSearchStage.filter = JSON.parse(opts.filter);
|
|
56
65
|
} catch (e) {
|
|
57
|
-
|
|
66
|
+
if (spin) spin.stop();
|
|
67
|
+
console.error(ui.error('Invalid filter JSON. Ensure it is valid JSON.'));
|
|
58
68
|
process.exit(1);
|
|
59
69
|
}
|
|
60
70
|
}
|
|
@@ -67,6 +77,8 @@ function registerSearch(program) {
|
|
|
67
77
|
|
|
68
78
|
const results = await collection.aggregate(pipeline).toArray();
|
|
69
79
|
|
|
80
|
+
if (spin) spin.stop();
|
|
81
|
+
|
|
70
82
|
const cleanResults = results.map(doc => {
|
|
71
83
|
const clean = { ...doc };
|
|
72
84
|
delete clean[opts.field];
|
|
@@ -79,28 +91,29 @@ function registerSearch(program) {
|
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
if (!opts.quiet) {
|
|
82
|
-
console.log(
|
|
83
|
-
console.log(
|
|
94
|
+
console.log(ui.label('Query', ui.cyan(`"${opts.query}"`)));
|
|
95
|
+
console.log(ui.label('Results', String(cleanResults.length)));
|
|
84
96
|
console.log('');
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
if (cleanResults.length === 0) {
|
|
88
|
-
console.log('No results found.');
|
|
100
|
+
console.log(ui.yellow('No results found.'));
|
|
89
101
|
return;
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
for (let i = 0; i < cleanResults.length; i++) {
|
|
93
105
|
const doc = cleanResults[i];
|
|
94
|
-
const
|
|
95
|
-
|
|
106
|
+
const scoreVal = doc.score;
|
|
107
|
+
const scoreStr = scoreVal != null ? ui.score(scoreVal) : 'N/A';
|
|
108
|
+
console.log(`── ${ui.bold('Result ' + (i + 1))} (score: ${scoreStr}) ──`);
|
|
96
109
|
const textPreview = doc.text ? doc.text.substring(0, 200) : 'No text field';
|
|
97
110
|
const ellipsis = doc.text && doc.text.length > 200 ? '...' : '';
|
|
98
111
|
console.log(` ${textPreview}${ellipsis}`);
|
|
99
|
-
console.log(` _id:
|
|
112
|
+
console.log(` ${ui.dim('_id: ' + doc._id)}`);
|
|
100
113
|
console.log('');
|
|
101
114
|
}
|
|
102
115
|
} catch (err) {
|
|
103
|
-
console.error(
|
|
116
|
+
console.error(ui.error(err.message));
|
|
104
117
|
process.exit(1);
|
|
105
118
|
} finally {
|
|
106
119
|
if (client) await client.close();
|
package/src/commands/store.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const {
|
|
4
|
+
const { getDefaultModel } = require('../lib/catalog');
|
|
5
5
|
const { generateEmbeddings } = require('../lib/api');
|
|
6
6
|
const { resolveTextInput } = require('../lib/input');
|
|
7
7
|
const { getMongoCollection } = require('../lib/mongo');
|
|
8
|
+
const ui = require('../lib/ui');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Register the store command on a Commander program.
|
|
@@ -19,7 +20,7 @@ function registerStore(program) {
|
|
|
19
20
|
.requiredOption('--field <name>', 'Embedding field name')
|
|
20
21
|
.option('--text <text>', 'Text to embed and store')
|
|
21
22
|
.option('-f, --file <path>', 'File to embed and store (text file or .jsonl for batch mode)')
|
|
22
|
-
.option('-m, --model <model>', 'Embedding model',
|
|
23
|
+
.option('-m, --model <model>', 'Embedding model', getDefaultModel())
|
|
23
24
|
.option('--input-type <type>', 'Input type: query or document', 'document')
|
|
24
25
|
.option('-d, --dimensions <n>', 'Output dimensions', (v) => parseInt(v, 10))
|
|
25
26
|
.option('--metadata <json>', 'Additional metadata as JSON')
|
|
@@ -37,6 +38,14 @@ function registerStore(program) {
|
|
|
37
38
|
const texts = await resolveTextInput(opts.text, opts.file);
|
|
38
39
|
const textContent = texts[0];
|
|
39
40
|
|
|
41
|
+
const useColor = !opts.json;
|
|
42
|
+
const useSpinner = useColor && !opts.quiet;
|
|
43
|
+
let spin;
|
|
44
|
+
if (useSpinner) {
|
|
45
|
+
spin = ui.spinner('Embedding and storing...');
|
|
46
|
+
spin.start();
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
const embedResult = await generateEmbeddings([textContent], {
|
|
41
50
|
model: opts.model,
|
|
42
51
|
inputType: opts.inputType,
|
|
@@ -48,7 +57,7 @@ function registerStore(program) {
|
|
|
48
57
|
const doc = {
|
|
49
58
|
text: textContent,
|
|
50
59
|
[opts.field]: embedding,
|
|
51
|
-
model: opts.model ||
|
|
60
|
+
model: opts.model || getDefaultModel(),
|
|
52
61
|
dimensions: embedding.length,
|
|
53
62
|
createdAt: new Date(),
|
|
54
63
|
};
|
|
@@ -58,7 +67,8 @@ function registerStore(program) {
|
|
|
58
67
|
const meta = JSON.parse(opts.metadata);
|
|
59
68
|
Object.assign(doc, meta);
|
|
60
69
|
} catch (e) {
|
|
61
|
-
|
|
70
|
+
if (spin) spin.stop();
|
|
71
|
+
console.error(ui.error('Invalid metadata JSON. Ensure it is valid JSON.'));
|
|
62
72
|
process.exit(1);
|
|
63
73
|
}
|
|
64
74
|
}
|
|
@@ -67,6 +77,8 @@ function registerStore(program) {
|
|
|
67
77
|
client = c;
|
|
68
78
|
const result = await collection.insertOne(doc);
|
|
69
79
|
|
|
80
|
+
if (spin) spin.stop();
|
|
81
|
+
|
|
70
82
|
if (opts.json) {
|
|
71
83
|
console.log(JSON.stringify({
|
|
72
84
|
insertedId: result.insertedId,
|
|
@@ -75,18 +87,18 @@ function registerStore(program) {
|
|
|
75
87
|
tokens: embedResult.usage?.total_tokens,
|
|
76
88
|
}, null, 2));
|
|
77
89
|
} else if (!opts.quiet) {
|
|
78
|
-
console.log(
|
|
79
|
-
console.log(
|
|
80
|
-
console.log(
|
|
81
|
-
console.log(
|
|
82
|
-
console.log(
|
|
83
|
-
console.log(
|
|
90
|
+
console.log(ui.success('Stored document: ' + ui.cyan(String(result.insertedId))));
|
|
91
|
+
console.log(ui.label('Database', opts.db));
|
|
92
|
+
console.log(ui.label('Collection', opts.collection));
|
|
93
|
+
console.log(ui.label('Field', opts.field));
|
|
94
|
+
console.log(ui.label('Dimensions', String(embedding.length)));
|
|
95
|
+
console.log(ui.label('Model', doc.model));
|
|
84
96
|
if (embedResult.usage) {
|
|
85
|
-
console.log(
|
|
97
|
+
console.log(ui.label('Tokens', String(embedResult.usage.total_tokens)));
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
100
|
} catch (err) {
|
|
89
|
-
console.error(
|
|
101
|
+
console.error(ui.error(err.message));
|
|
90
102
|
process.exit(1);
|
|
91
103
|
} finally {
|
|
92
104
|
if (client) await client.close();
|
|
@@ -106,7 +118,7 @@ async function handleBatchStore(opts) {
|
|
|
106
118
|
const lines = content.split('\n').filter(line => line.trim());
|
|
107
119
|
|
|
108
120
|
if (lines.length === 0) {
|
|
109
|
-
console.error('
|
|
121
|
+
console.error(ui.error('JSONL file is empty.'));
|
|
110
122
|
process.exit(1);
|
|
111
123
|
}
|
|
112
124
|
|
|
@@ -114,21 +126,25 @@ async function handleBatchStore(opts) {
|
|
|
114
126
|
try {
|
|
115
127
|
return JSON.parse(line);
|
|
116
128
|
} catch (e) {
|
|
117
|
-
console.error(`
|
|
129
|
+
console.error(ui.error(`Invalid JSON on line ${i + 1}: ${e.message}`));
|
|
118
130
|
process.exit(1);
|
|
119
131
|
}
|
|
120
132
|
});
|
|
121
133
|
|
|
122
134
|
const texts = records.map(r => {
|
|
123
135
|
if (!r.text) {
|
|
124
|
-
console.error('
|
|
136
|
+
console.error(ui.error('Each JSONL line must have a "text" field.'));
|
|
125
137
|
process.exit(1);
|
|
126
138
|
}
|
|
127
139
|
return r.text;
|
|
128
140
|
});
|
|
129
141
|
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
const useColor = !opts.json;
|
|
143
|
+
const useSpinner = useColor && !opts.quiet;
|
|
144
|
+
let spin;
|
|
145
|
+
if (useSpinner) {
|
|
146
|
+
spin = ui.spinner(`Embedding and storing ${texts.length} documents...`);
|
|
147
|
+
spin.start();
|
|
132
148
|
}
|
|
133
149
|
|
|
134
150
|
const embedResult = await generateEmbeddings(texts, {
|
|
@@ -142,7 +158,7 @@ async function handleBatchStore(opts) {
|
|
|
142
158
|
const doc = {
|
|
143
159
|
text: record.text,
|
|
144
160
|
[opts.field]: embedding,
|
|
145
|
-
model: opts.model ||
|
|
161
|
+
model: opts.model || getDefaultModel(),
|
|
146
162
|
dimensions: embedding.length,
|
|
147
163
|
createdAt: new Date(),
|
|
148
164
|
};
|
|
@@ -156,27 +172,29 @@ async function handleBatchStore(opts) {
|
|
|
156
172
|
client = c;
|
|
157
173
|
const result = await collection.insertMany(docs);
|
|
158
174
|
|
|
175
|
+
if (spin) spin.stop();
|
|
176
|
+
|
|
159
177
|
if (opts.json) {
|
|
160
178
|
console.log(JSON.stringify({
|
|
161
179
|
insertedCount: result.insertedCount,
|
|
162
180
|
insertedIds: result.insertedIds,
|
|
163
181
|
dimensions: docs[0]?.dimensions,
|
|
164
|
-
model: opts.model ||
|
|
182
|
+
model: opts.model || getDefaultModel(),
|
|
165
183
|
tokens: embedResult.usage?.total_tokens,
|
|
166
184
|
}, null, 2));
|
|
167
185
|
} else if (!opts.quiet) {
|
|
168
|
-
console.log(
|
|
169
|
-
console.log(
|
|
170
|
-
console.log(
|
|
171
|
-
console.log(
|
|
172
|
-
console.log(
|
|
173
|
-
console.log(
|
|
186
|
+
console.log(ui.success(`Stored ${result.insertedCount} documents`));
|
|
187
|
+
console.log(ui.label('Database', opts.db));
|
|
188
|
+
console.log(ui.label('Collection', opts.collection));
|
|
189
|
+
console.log(ui.label('Field', opts.field));
|
|
190
|
+
console.log(ui.label('Dimensions', String(docs[0]?.dimensions)));
|
|
191
|
+
console.log(ui.label('Model', opts.model || getDefaultModel()));
|
|
174
192
|
if (embedResult.usage) {
|
|
175
|
-
console.log(
|
|
193
|
+
console.log(ui.label('Tokens', String(embedResult.usage.total_tokens)));
|
|
176
194
|
}
|
|
177
195
|
}
|
|
178
196
|
} catch (err) {
|
|
179
|
-
console.error(
|
|
197
|
+
console.error(ui.error(err.message));
|
|
180
198
|
process.exit(1);
|
|
181
199
|
} finally {
|
|
182
200
|
if (client) await client.close();
|
package/src/lib/api.js
CHANGED
|
@@ -5,15 +5,19 @@ const MAX_RETRIES = 3;
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Get the Voyage API key or exit with a helpful error.
|
|
8
|
+
* Checks: env var → config file.
|
|
8
9
|
* @returns {string}
|
|
9
10
|
*/
|
|
10
11
|
function requireApiKey() {
|
|
11
|
-
const
|
|
12
|
+
const { getConfigValue } = require('./config');
|
|
13
|
+
const key = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
|
|
12
14
|
if (!key) {
|
|
13
|
-
console.error('Error: VOYAGE_API_KEY
|
|
15
|
+
console.error('Error: VOYAGE_API_KEY is not set.');
|
|
16
|
+
console.error('');
|
|
17
|
+
console.error('Option 1: export VOYAGE_API_KEY="your-key-here"');
|
|
18
|
+
console.error('Option 2: vai config set api-key <your-key>');
|
|
14
19
|
console.error('');
|
|
15
20
|
console.error('Get one from MongoDB Atlas → AI Models → Create model API key');
|
|
16
|
-
console.error('Then: export VOYAGE_API_KEY="your-key-here"');
|
|
17
21
|
process.exit(1);
|
|
18
22
|
}
|
|
19
23
|
return key;
|
|
@@ -82,11 +86,11 @@ async function apiRequest(endpoint, body) {
|
|
|
82
86
|
* @returns {Promise<object>} API response with embeddings
|
|
83
87
|
*/
|
|
84
88
|
async function generateEmbeddings(texts, options = {}) {
|
|
85
|
-
const {
|
|
89
|
+
const { getDefaultModel } = require('./catalog');
|
|
86
90
|
|
|
87
91
|
const body = {
|
|
88
92
|
input: texts,
|
|
89
|
-
model: options.model ||
|
|
93
|
+
model: options.model || getDefaultModel(),
|
|
90
94
|
};
|
|
91
95
|
|
|
92
96
|
if (options.inputType) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pc = require('picocolors');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read the package version from package.json.
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
function getVersion() {
|
|
10
|
+
const pkg = require('../../package.json');
|
|
11
|
+
return pkg.version || '0.0.0';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Display a compact ASCII banner for the CLI.
|
|
16
|
+
*/
|
|
17
|
+
function showBanner() {
|
|
18
|
+
const version = getVersion();
|
|
19
|
+
const title = ` 🧭 ${pc.bold(pc.cyan('vai'))} — ${pc.bold('Voyage AI CLI')} ${pc.dim('v' + version)}`;
|
|
20
|
+
const tagline = ` ${pc.dim('Embeddings, reranking & search')}`;
|
|
21
|
+
|
|
22
|
+
// Calculate visible width (strip ANSI codes for alignment)
|
|
23
|
+
const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
24
|
+
const titleLen = stripAnsi(title).length;
|
|
25
|
+
const taglineLen = stripAnsi(tagline).length;
|
|
26
|
+
const innerWidth = Math.max(titleLen, taglineLen) + 2;
|
|
27
|
+
|
|
28
|
+
const top = pc.dim(' ╭' + '─'.repeat(innerWidth) + '╮');
|
|
29
|
+
const bot = pc.dim(' ╰' + '─'.repeat(innerWidth) + '╯');
|
|
30
|
+
const titleLine = pc.dim(' │') + title + ' '.repeat(innerWidth - titleLen) + pc.dim('│');
|
|
31
|
+
const taglineLine = pc.dim(' │') + tagline + ' '.repeat(innerWidth - taglineLen) + pc.dim('│');
|
|
32
|
+
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(top);
|
|
35
|
+
console.log(titleLine);
|
|
36
|
+
console.log(taglineLine);
|
|
37
|
+
console.log(bot);
|
|
38
|
+
console.log('');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Display the quick start guide with colored commands.
|
|
43
|
+
*/
|
|
44
|
+
function showQuickStart() {
|
|
45
|
+
console.log(` ${pc.bold('Quick start:')}`);
|
|
46
|
+
console.log(` ${pc.cyan('$ vai ping')} Test your connection`);
|
|
47
|
+
console.log(` ${pc.cyan('$ vai embed "hello world"')} Generate embeddings`);
|
|
48
|
+
console.log(` ${pc.cyan('$ vai models')} List available models`);
|
|
49
|
+
console.log(` ${pc.cyan('$ vai demo')} Interactive walkthrough`);
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(` Run ${pc.cyan('vai <command> --help')} for detailed usage.`);
|
|
52
|
+
console.log('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { showBanner, showQuickStart, getVersion };
|
package/src/lib/catalog.js
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { getConfigValue } = require('./config');
|
|
4
|
+
|
|
3
5
|
const DEFAULT_EMBED_MODEL = 'voyage-4-large';
|
|
4
6
|
const DEFAULT_RERANK_MODEL = 'rerank-2.5';
|
|
5
7
|
const DEFAULT_DIMENSIONS = 1024;
|
|
6
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Get the default embedding model (config override or built-in default).
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function getDefaultModel() {
|
|
14
|
+
return getConfigValue('defaultModel') || DEFAULT_EMBED_MODEL;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the default dimensions (config override or built-in default).
|
|
19
|
+
* @returns {number}
|
|
20
|
+
*/
|
|
21
|
+
function getDefaultDimensions() {
|
|
22
|
+
return getConfigValue('defaultDimensions') || DEFAULT_DIMENSIONS;
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
/** @type {Array<{name: string, type: string, context: string, dimensions: string, price: string, bestFor: string}>} */
|
|
8
26
|
const MODEL_CATALOG = [
|
|
9
27
|
{ name: 'voyage-4-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/1M tokens', bestFor: 'Best quality, multilingual' },
|
|
@@ -22,5 +40,7 @@ module.exports = {
|
|
|
22
40
|
DEFAULT_EMBED_MODEL,
|
|
23
41
|
DEFAULT_RERANK_MODEL,
|
|
24
42
|
DEFAULT_DIMENSIONS,
|
|
43
|
+
getDefaultModel,
|
|
44
|
+
getDefaultDimensions,
|
|
25
45
|
MODEL_CATALOG,
|
|
26
46
|
};
|