voyageai-cli 1.4.0 → 1.5.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/README.md +38 -0
- package/demo.gif +0 -0
- package/demo.tape +39 -0
- package/package.json +1 -1
- package/scripts/record-demo.sh +63 -0
- package/src/cli.js +2 -0
- package/src/commands/completions.js +463 -0
- package/src/commands/models.js +61 -13
- package/src/commands/ping.js +5 -4
- package/src/lib/api.js +48 -2
- package/src/lib/catalog.js +10 -10
- package/src/lib/config.js +1 -0
- package/test/commands/completions.test.js +166 -0
- package/test/commands/ping.test.js +24 -11
- package/test/lib/api.test.js +12 -3
package/README.md
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
CLI for [Voyage AI](https://www.mongodb.com/docs/voyageai/) embeddings, reranking, and [MongoDB Atlas Vector Search](https://www.mongodb.com/docs/atlas/atlas-vector-search/). Pure Node.js — no Python required.
|
|
6
6
|
|
|
7
|
+
<!-- TODO: Add demo GIF -->
|
|
8
|
+
<!--  -->
|
|
9
|
+
|
|
7
10
|
Generate embeddings, rerank search results, store vectors in Atlas, and run semantic search — all from the command line.
|
|
8
11
|
|
|
9
12
|
> **⚠️ Disclaimer:** This is an independent, community-built tool. It is **not** an official product of MongoDB, Inc. or Voyage AI. It is not supported, endorsed, or maintained by either company. For official documentation, support, and products, visit:
|
|
@@ -248,6 +251,41 @@ vai config get
|
|
|
248
251
|
- Use `echo "key" | vai config set api-key --stdin` or `vai config set api-key --stdin < keyfile` to avoid shell history exposure
|
|
249
252
|
- The config file stores credentials in plaintext (similar to `~/.aws/credentials` and `~/.npmrc`) — protect your home directory accordingly
|
|
250
253
|
|
|
254
|
+
## Shell Completions
|
|
255
|
+
|
|
256
|
+
`vai` supports tab completion for bash and zsh.
|
|
257
|
+
|
|
258
|
+
### Bash
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# Add to ~/.bashrc (or ~/.bash_profile on macOS)
|
|
262
|
+
vai completions bash >> ~/.bashrc
|
|
263
|
+
source ~/.bashrc
|
|
264
|
+
|
|
265
|
+
# Or install system-wide (Linux)
|
|
266
|
+
vai completions bash > /etc/bash_completion.d/vai
|
|
267
|
+
|
|
268
|
+
# Or with Homebrew (macOS)
|
|
269
|
+
vai completions bash > $(brew --prefix)/etc/bash_completion.d/vai
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Zsh
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# Create completions directory
|
|
276
|
+
mkdir -p ~/.zsh/completions
|
|
277
|
+
|
|
278
|
+
# Add to fpath in ~/.zshrc (if not already there)
|
|
279
|
+
echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc
|
|
280
|
+
echo 'autoload -Uz compinit && compinit' >> ~/.zshrc
|
|
281
|
+
|
|
282
|
+
# Generate the completion file
|
|
283
|
+
vai completions zsh > ~/.zsh/completions/_vai
|
|
284
|
+
source ~/.zshrc
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Completions cover all 14 commands, subcommands, flags, model names, and explain topics.
|
|
288
|
+
|
|
251
289
|
## Global Flags
|
|
252
290
|
|
|
253
291
|
All commands support:
|
package/demo.gif
ADDED
|
Binary file
|
package/demo.tape
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# VHS demo tape for voyageai-cli
|
|
2
|
+
# Run: vhs demo.tape
|
|
3
|
+
# Requires: VOYAGE_API_KEY set in environment
|
|
4
|
+
|
|
5
|
+
Output demo.gif
|
|
6
|
+
|
|
7
|
+
Set FontSize 16
|
|
8
|
+
Set Width 900
|
|
9
|
+
Set Height 600
|
|
10
|
+
Set Theme "Catppuccin Mocha"
|
|
11
|
+
Set Padding 20
|
|
12
|
+
|
|
13
|
+
# Show version
|
|
14
|
+
Type "vai --version"
|
|
15
|
+
Enter
|
|
16
|
+
Sleep 1.5s
|
|
17
|
+
|
|
18
|
+
# List embedding models
|
|
19
|
+
Type "vai models --type embedding"
|
|
20
|
+
Enter
|
|
21
|
+
Sleep 3s
|
|
22
|
+
|
|
23
|
+
# Generate an embedding
|
|
24
|
+
Type 'vai embed "What is MongoDB Atlas?"'
|
|
25
|
+
Enter
|
|
26
|
+
Sleep 4s
|
|
27
|
+
|
|
28
|
+
# Explain embeddings (first few lines)
|
|
29
|
+
Type "vai explain embeddings"
|
|
30
|
+
Enter
|
|
31
|
+
Sleep 4s
|
|
32
|
+
|
|
33
|
+
# Compare similarity
|
|
34
|
+
Type 'vai similarity "MongoDB is great" "MongoDB Atlas is amazing"'
|
|
35
|
+
Enter
|
|
36
|
+
Sleep 4s
|
|
37
|
+
|
|
38
|
+
# Pause at end to show results
|
|
39
|
+
Sleep 2s
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Record a demo GIF for voyageai-cli
|
|
3
|
+
# Requires: VOYAGE_API_KEY environment variable
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# ./scripts/record-demo.sh # Uses vhs (preferred)
|
|
7
|
+
# ./scripts/record-demo.sh asciinema # Uses asciinema instead
|
|
8
|
+
#
|
|
9
|
+
# Output:
|
|
10
|
+
# demo.gif (vhs) or demo.cast (asciinema)
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
cd "$(dirname "$0")/.."
|
|
14
|
+
|
|
15
|
+
METHOD="${1:-vhs}"
|
|
16
|
+
|
|
17
|
+
if [ "$METHOD" = "vhs" ]; then
|
|
18
|
+
if ! command -v vhs &>/dev/null; then
|
|
19
|
+
echo "❌ vhs not found. Install: brew install charmbracelet/tap/vhs"
|
|
20
|
+
echo " Or run: ./scripts/record-demo.sh asciinema"
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [ -z "${VOYAGE_API_KEY:-}" ]; then
|
|
25
|
+
echo "⚠️ VOYAGE_API_KEY not set. Commands that call the API will fail."
|
|
26
|
+
echo " Set it: export VOYAGE_API_KEY=your-key"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo "🎬 Recording demo with vhs..."
|
|
31
|
+
vhs demo.tape
|
|
32
|
+
echo "✅ Demo GIF saved to demo.gif"
|
|
33
|
+
|
|
34
|
+
elif [ "$METHOD" = "asciinema" ]; then
|
|
35
|
+
if ! command -v asciinema &>/dev/null; then
|
|
36
|
+
echo "❌ asciinema not found. Install: brew install asciinema"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
CAST_FILE="demo.cast"
|
|
41
|
+
echo "🎬 Recording demo with asciinema..."
|
|
42
|
+
echo " Run the following commands, then press Ctrl-D when done:"
|
|
43
|
+
echo ""
|
|
44
|
+
echo " vai --version"
|
|
45
|
+
echo " vai models --type embedding"
|
|
46
|
+
echo ' vai embed "What is MongoDB Atlas?"'
|
|
47
|
+
echo " vai explain embeddings"
|
|
48
|
+
echo ' vai similarity "MongoDB is great" "MongoDB Atlas is amazing"'
|
|
49
|
+
echo ""
|
|
50
|
+
|
|
51
|
+
asciinema rec "$CAST_FILE"
|
|
52
|
+
echo "✅ Recording saved to $CAST_FILE"
|
|
53
|
+
echo ""
|
|
54
|
+
echo "Convert to GIF with agg or svg-term-cli:"
|
|
55
|
+
echo " agg $CAST_FILE demo.gif"
|
|
56
|
+
echo " # or"
|
|
57
|
+
echo " npx svg-term-cli --in $CAST_FILE --out demo.svg --window"
|
|
58
|
+
|
|
59
|
+
else
|
|
60
|
+
echo "Unknown method: $METHOD"
|
|
61
|
+
echo "Usage: $0 [vhs|asciinema]"
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
package/src/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ const { registerDemo } = require('./commands/demo');
|
|
|
17
17
|
const { registerExplain } = require('./commands/explain');
|
|
18
18
|
const { registerSimilarity } = require('./commands/similarity');
|
|
19
19
|
const { registerIngest } = require('./commands/ingest');
|
|
20
|
+
const { registerCompletions } = require('./commands/completions');
|
|
20
21
|
const { showBanner, showQuickStart, getVersion } = require('./lib/banner');
|
|
21
22
|
|
|
22
23
|
const version = getVersion();
|
|
@@ -38,6 +39,7 @@ registerDemo(program);
|
|
|
38
39
|
registerExplain(program);
|
|
39
40
|
registerSimilarity(program);
|
|
40
41
|
registerIngest(program);
|
|
42
|
+
registerCompletions(program);
|
|
41
43
|
|
|
42
44
|
// Append disclaimer to all help output
|
|
43
45
|
program.addHelpText('after', `
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pc = require('picocolors');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate bash completion script for vai CLI.
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
function generateBashCompletions() {
|
|
10
|
+
return `#!/bin/bash
|
|
11
|
+
# vai bash completion script
|
|
12
|
+
# Install: vai completions bash >> ~/.bashrc && source ~/.bashrc
|
|
13
|
+
# Or: vai completions bash > /usr/local/etc/bash_completion.d/vai
|
|
14
|
+
|
|
15
|
+
_vai_completions() {
|
|
16
|
+
local cur prev commands
|
|
17
|
+
COMPREPLY=()
|
|
18
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
19
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
20
|
+
|
|
21
|
+
# Top-level commands
|
|
22
|
+
commands="embed rerank store search index models ping config demo explain similarity ingest completions help"
|
|
23
|
+
|
|
24
|
+
# Subcommands
|
|
25
|
+
local index_subs="create list delete"
|
|
26
|
+
local config_subs="set get delete path reset"
|
|
27
|
+
|
|
28
|
+
# Global flags
|
|
29
|
+
local global_flags="--json --quiet --help --version"
|
|
30
|
+
|
|
31
|
+
case "\${COMP_WORDS[1]}" in
|
|
32
|
+
embed)
|
|
33
|
+
COMPREPLY=( \$(compgen -W "--model --input-type --dimensions --file --output-format --json --quiet --help" -- "\$cur") )
|
|
34
|
+
return 0
|
|
35
|
+
;;
|
|
36
|
+
rerank)
|
|
37
|
+
COMPREPLY=( \$(compgen -W "--query --documents --documents-file --model --top-k --json --quiet --help" -- "\$cur") )
|
|
38
|
+
return 0
|
|
39
|
+
;;
|
|
40
|
+
store)
|
|
41
|
+
COMPREPLY=( \$(compgen -W "--db --collection --field --text --file --model --input-type --dimensions --metadata --json --quiet --help" -- "\$cur") )
|
|
42
|
+
return 0
|
|
43
|
+
;;
|
|
44
|
+
search)
|
|
45
|
+
COMPREPLY=( \$(compgen -W "--query --db --collection --index --field --model --input-type --dimensions --limit --min-score --num-candidates --filter --json --quiet --help" -- "\$cur") )
|
|
46
|
+
return 0
|
|
47
|
+
;;
|
|
48
|
+
index)
|
|
49
|
+
if [[ \$COMP_CWORD -eq 2 ]]; then
|
|
50
|
+
COMPREPLY=( \$(compgen -W "\$index_subs" -- "\$cur") )
|
|
51
|
+
else
|
|
52
|
+
case "\${COMP_WORDS[2]}" in
|
|
53
|
+
create)
|
|
54
|
+
COMPREPLY=( \$(compgen -W "--db --collection --field --dimensions --similarity --index-name --json --quiet --help" -- "\$cur") )
|
|
55
|
+
;;
|
|
56
|
+
list)
|
|
57
|
+
COMPREPLY=( \$(compgen -W "--db --collection --json --quiet --help" -- "\$cur") )
|
|
58
|
+
;;
|
|
59
|
+
delete)
|
|
60
|
+
COMPREPLY=( \$(compgen -W "--db --collection --index-name --json --quiet --help" -- "\$cur") )
|
|
61
|
+
;;
|
|
62
|
+
esac
|
|
63
|
+
fi
|
|
64
|
+
return 0
|
|
65
|
+
;;
|
|
66
|
+
models)
|
|
67
|
+
COMPREPLY=( \$(compgen -W "--type --json --quiet --help" -- "\$cur") )
|
|
68
|
+
return 0
|
|
69
|
+
;;
|
|
70
|
+
ping)
|
|
71
|
+
COMPREPLY=( \$(compgen -W "--json --quiet --help" -- "\$cur") )
|
|
72
|
+
return 0
|
|
73
|
+
;;
|
|
74
|
+
config)
|
|
75
|
+
if [[ \$COMP_CWORD -eq 2 ]]; then
|
|
76
|
+
COMPREPLY=( \$(compgen -W "\$config_subs" -- "\$cur") )
|
|
77
|
+
else
|
|
78
|
+
case "\${COMP_WORDS[2]}" in
|
|
79
|
+
set)
|
|
80
|
+
COMPREPLY=( \$(compgen -W "api-key mongodb-uri base-url default-model --stdin --help" -- "\$cur") )
|
|
81
|
+
;;
|
|
82
|
+
get|delete)
|
|
83
|
+
COMPREPLY=( \$(compgen -W "api-key mongodb-uri base-url default-model --help" -- "\$cur") )
|
|
84
|
+
;;
|
|
85
|
+
esac
|
|
86
|
+
fi
|
|
87
|
+
return 0
|
|
88
|
+
;;
|
|
89
|
+
demo)
|
|
90
|
+
COMPREPLY=( \$(compgen -W "--no-pause --skip-pipeline --keep --json --quiet --help" -- "\$cur") )
|
|
91
|
+
return 0
|
|
92
|
+
;;
|
|
93
|
+
explain)
|
|
94
|
+
COMPREPLY=( \$(compgen -W "embeddings reranking vector-search rag cosine-similarity two-stage-retrieval input-type models api-keys api-access batch-processing --help" -- "\$cur") )
|
|
95
|
+
return 0
|
|
96
|
+
;;
|
|
97
|
+
similarity)
|
|
98
|
+
COMPREPLY=( \$(compgen -W "--against --file1 --file2 --model --dimensions --json --quiet --help" -- "\$cur") )
|
|
99
|
+
return 0
|
|
100
|
+
;;
|
|
101
|
+
ingest)
|
|
102
|
+
COMPREPLY=( \$(compgen -W "--file --db --collection --field --model --input-type --dimensions --batch-size --text-field --text-column --strict --dry-run --json --quiet --help" -- "\$cur") )
|
|
103
|
+
return 0
|
|
104
|
+
;;
|
|
105
|
+
completions)
|
|
106
|
+
COMPREPLY=( \$(compgen -W "bash zsh --help" -- "\$cur") )
|
|
107
|
+
return 0
|
|
108
|
+
;;
|
|
109
|
+
esac
|
|
110
|
+
|
|
111
|
+
# Complete top-level commands and global flags
|
|
112
|
+
if [[ \$COMP_CWORD -eq 1 ]]; then
|
|
113
|
+
COMPREPLY=( \$(compgen -W "\$commands \$global_flags" -- "\$cur") )
|
|
114
|
+
return 0
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# Model name completions
|
|
118
|
+
case "\$prev" in
|
|
119
|
+
--model|-m)
|
|
120
|
+
COMPREPLY=( \$(compgen -W "voyage-4-large voyage-4 voyage-4-lite voyage-code-3 voyage-finance-2 voyage-law-2 voyage-multimodal-3.5 rerank-2.5 rerank-2.5-lite" -- "\$cur") )
|
|
121
|
+
return 0
|
|
122
|
+
;;
|
|
123
|
+
--input-type)
|
|
124
|
+
COMPREPLY=( \$(compgen -W "query document" -- "\$cur") )
|
|
125
|
+
return 0
|
|
126
|
+
;;
|
|
127
|
+
--type|-t)
|
|
128
|
+
COMPREPLY=( \$(compgen -W "embedding reranking all" -- "\$cur") )
|
|
129
|
+
return 0
|
|
130
|
+
;;
|
|
131
|
+
--similarity|-s)
|
|
132
|
+
COMPREPLY=( \$(compgen -W "cosine dotProduct euclidean" -- "\$cur") )
|
|
133
|
+
return 0
|
|
134
|
+
;;
|
|
135
|
+
--output-format|-o)
|
|
136
|
+
COMPREPLY=( \$(compgen -W "json array" -- "\$cur") )
|
|
137
|
+
return 0
|
|
138
|
+
;;
|
|
139
|
+
--file|-f|--documents-file|--file1|--file2)
|
|
140
|
+
COMPREPLY=( \$(compgen -f -- "\$cur") )
|
|
141
|
+
return 0
|
|
142
|
+
;;
|
|
143
|
+
esac
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
complete -F _vai_completions vai
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate zsh completion script for vai CLI.
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
function generateZshCompletions() {
|
|
155
|
+
return `#compdef vai
|
|
156
|
+
# vai zsh completion script
|
|
157
|
+
# Install: vai completions zsh > ~/.zsh/completions/_vai && source ~/.zshrc
|
|
158
|
+
# Or: vai completions zsh > /usr/local/share/zsh/site-functions/_vai
|
|
159
|
+
|
|
160
|
+
_vai() {
|
|
161
|
+
local -a commands
|
|
162
|
+
commands=(
|
|
163
|
+
'embed:Generate embeddings for text'
|
|
164
|
+
'rerank:Rerank documents against a query'
|
|
165
|
+
'store:Embed text and store in MongoDB Atlas'
|
|
166
|
+
'search:Vector search against Atlas collection'
|
|
167
|
+
'index:Manage Atlas Vector Search indexes'
|
|
168
|
+
'models:List available Voyage AI models'
|
|
169
|
+
'ping:Test connectivity to Voyage AI API'
|
|
170
|
+
'config:Manage persistent configuration'
|
|
171
|
+
'demo:Interactive guided walkthrough'
|
|
172
|
+
'explain:Learn about AI and vector search concepts'
|
|
173
|
+
'similarity:Compute cosine similarity between texts'
|
|
174
|
+
'ingest:Bulk import documents with progress'
|
|
175
|
+
'completions:Generate shell completion scripts'
|
|
176
|
+
'help:Display help for command'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
local -a models
|
|
180
|
+
models=(voyage-4-large voyage-4 voyage-4-lite voyage-code-3 voyage-finance-2 voyage-law-2 voyage-multimodal-3.5 rerank-2.5 rerank-2.5-lite)
|
|
181
|
+
|
|
182
|
+
local -a explain_topics
|
|
183
|
+
explain_topics=(embeddings reranking vector-search rag cosine-similarity two-stage-retrieval input-type models api-keys api-access batch-processing)
|
|
184
|
+
|
|
185
|
+
_arguments -C \\
|
|
186
|
+
'(-V --version)'{-V,--version}'[output the version number]' \\
|
|
187
|
+
'(-h --help)'{-h,--help}'[display help]' \\
|
|
188
|
+
'1:command:->command' \\
|
|
189
|
+
'*::arg:->args'
|
|
190
|
+
|
|
191
|
+
case \$state in
|
|
192
|
+
command)
|
|
193
|
+
_describe 'vai command' commands
|
|
194
|
+
;;
|
|
195
|
+
args)
|
|
196
|
+
case \$words[1] in
|
|
197
|
+
embed)
|
|
198
|
+
_arguments \\
|
|
199
|
+
'(-m --model)'{-m,--model}'[Embedding model]:model:(\$models)' \\
|
|
200
|
+
'(-t --input-type)'{-t,--input-type}'[Input type]:type:(query document)' \\
|
|
201
|
+
'(-d --dimensions)'{-d,--dimensions}'[Output dimensions]:dimensions:' \\
|
|
202
|
+
'(-f --file)'{-f,--file}'[Read text from file]:file:_files' \\
|
|
203
|
+
'(-o --output-format)'{-o,--output-format}'[Output format]:format:(json array)' \\
|
|
204
|
+
'--json[Machine-readable JSON output]' \\
|
|
205
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]' \\
|
|
206
|
+
'1:text:'
|
|
207
|
+
;;
|
|
208
|
+
rerank)
|
|
209
|
+
_arguments \\
|
|
210
|
+
'--query[Search query]:query:' \\
|
|
211
|
+
'--documents[Documents to rerank]:document:' \\
|
|
212
|
+
'--documents-file[File with documents]:file:_files' \\
|
|
213
|
+
'(-m --model)'{-m,--model}'[Reranking model]:model:(\$models)' \\
|
|
214
|
+
'(-k --top-k)'{-k,--top-k}'[Return top K results]:k:' \\
|
|
215
|
+
'--json[Machine-readable JSON output]' \\
|
|
216
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
217
|
+
;;
|
|
218
|
+
store)
|
|
219
|
+
_arguments \\
|
|
220
|
+
'--db[Database name]:database:' \\
|
|
221
|
+
'--collection[Collection name]:collection:' \\
|
|
222
|
+
'--field[Embedding field name]:field:' \\
|
|
223
|
+
'--text[Text to embed and store]:text:' \\
|
|
224
|
+
'(-f --file)'{-f,--file}'[File to embed and store]:file:_files' \\
|
|
225
|
+
'(-m --model)'{-m,--model}'[Embedding model]:model:(\$models)' \\
|
|
226
|
+
'--input-type[Input type]:type:(query document)' \\
|
|
227
|
+
'(-d --dimensions)'{-d,--dimensions}'[Output dimensions]:dimensions:' \\
|
|
228
|
+
'--metadata[Additional metadata as JSON]:json:' \\
|
|
229
|
+
'--json[Machine-readable JSON output]' \\
|
|
230
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
231
|
+
;;
|
|
232
|
+
search)
|
|
233
|
+
_arguments \\
|
|
234
|
+
'--query[Search query text]:query:' \\
|
|
235
|
+
'--db[Database name]:database:' \\
|
|
236
|
+
'--collection[Collection name]:collection:' \\
|
|
237
|
+
'--index[Vector search index name]:index:' \\
|
|
238
|
+
'--field[Embedding field name]:field:' \\
|
|
239
|
+
'(-m --model)'{-m,--model}'[Embedding model]:model:(\$models)' \\
|
|
240
|
+
'--input-type[Input type]:type:(query document)' \\
|
|
241
|
+
'(-d --dimensions)'{-d,--dimensions}'[Output dimensions]:dimensions:' \\
|
|
242
|
+
'(-l --limit)'{-l,--limit}'[Maximum results]:limit:' \\
|
|
243
|
+
'--min-score[Minimum similarity score]:score:' \\
|
|
244
|
+
'--num-candidates[Number of ANN candidates]:n:' \\
|
|
245
|
+
'--filter[Pre-filter JSON]:json:' \\
|
|
246
|
+
'--json[Machine-readable JSON output]' \\
|
|
247
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
248
|
+
;;
|
|
249
|
+
index)
|
|
250
|
+
local -a index_commands
|
|
251
|
+
index_commands=(
|
|
252
|
+
'create:Create a vector search index'
|
|
253
|
+
'list:List vector search indexes'
|
|
254
|
+
'delete:Delete a vector search index'
|
|
255
|
+
)
|
|
256
|
+
_arguments -C \\
|
|
257
|
+
'1:index command:->index_command' \\
|
|
258
|
+
'*::arg:->index_args'
|
|
259
|
+
case \$state in
|
|
260
|
+
index_command)
|
|
261
|
+
_describe 'index command' index_commands
|
|
262
|
+
;;
|
|
263
|
+
index_args)
|
|
264
|
+
case \$words[1] in
|
|
265
|
+
create)
|
|
266
|
+
_arguments \\
|
|
267
|
+
'--db[Database name]:database:' \\
|
|
268
|
+
'--collection[Collection name]:collection:' \\
|
|
269
|
+
'--field[Embedding field name]:field:' \\
|
|
270
|
+
'(-d --dimensions)'{-d,--dimensions}'[Vector dimensions]:dimensions:' \\
|
|
271
|
+
'(-s --similarity)'{-s,--similarity}'[Similarity function]:similarity:(cosine dotProduct euclidean)' \\
|
|
272
|
+
'(-n --index-name)'{-n,--index-name}'[Index name]:name:' \\
|
|
273
|
+
'--json[Machine-readable JSON output]' \\
|
|
274
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
275
|
+
;;
|
|
276
|
+
list)
|
|
277
|
+
_arguments \\
|
|
278
|
+
'--db[Database name]:database:' \\
|
|
279
|
+
'--collection[Collection name]:collection:' \\
|
|
280
|
+
'--json[Machine-readable JSON output]' \\
|
|
281
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
282
|
+
;;
|
|
283
|
+
delete)
|
|
284
|
+
_arguments \\
|
|
285
|
+
'--db[Database name]:database:' \\
|
|
286
|
+
'--collection[Collection name]:collection:' \\
|
|
287
|
+
'--index-name[Index name]:name:' \\
|
|
288
|
+
'--json[Machine-readable JSON output]' \\
|
|
289
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
290
|
+
;;
|
|
291
|
+
esac
|
|
292
|
+
;;
|
|
293
|
+
esac
|
|
294
|
+
;;
|
|
295
|
+
models)
|
|
296
|
+
_arguments \\
|
|
297
|
+
'(-t --type)'{-t,--type}'[Filter by type]:type:(embedding reranking all)' \\
|
|
298
|
+
'--json[Machine-readable JSON output]' \\
|
|
299
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
300
|
+
;;
|
|
301
|
+
ping)
|
|
302
|
+
_arguments \\
|
|
303
|
+
'--json[Machine-readable JSON output]' \\
|
|
304
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
305
|
+
;;
|
|
306
|
+
config)
|
|
307
|
+
local -a config_commands
|
|
308
|
+
config_commands=(
|
|
309
|
+
'set:Set a config value'
|
|
310
|
+
'get:Show current configuration'
|
|
311
|
+
'delete:Remove a config value'
|
|
312
|
+
'path:Show config file path'
|
|
313
|
+
'reset:Reset all configuration'
|
|
314
|
+
)
|
|
315
|
+
_arguments -C \\
|
|
316
|
+
'1:config command:->config_command' \\
|
|
317
|
+
'*::arg:->config_args'
|
|
318
|
+
case \$state in
|
|
319
|
+
config_command)
|
|
320
|
+
_describe 'config command' config_commands
|
|
321
|
+
;;
|
|
322
|
+
config_args)
|
|
323
|
+
case \$words[1] in
|
|
324
|
+
set)
|
|
325
|
+
_arguments \\
|
|
326
|
+
'1:key:(api-key mongodb-uri base-url default-model)' \\
|
|
327
|
+
'2:value:' \\
|
|
328
|
+
'--stdin[Read value from stdin]'
|
|
329
|
+
;;
|
|
330
|
+
delete)
|
|
331
|
+
_arguments \\
|
|
332
|
+
'1:key:(api-key mongodb-uri base-url default-model)'
|
|
333
|
+
;;
|
|
334
|
+
esac
|
|
335
|
+
;;
|
|
336
|
+
esac
|
|
337
|
+
;;
|
|
338
|
+
demo)
|
|
339
|
+
_arguments \\
|
|
340
|
+
'--no-pause[Skip pauses between steps]' \\
|
|
341
|
+
'--skip-pipeline[Skip MongoDB pipeline steps]' \\
|
|
342
|
+
'--keep[Keep demo data after completion]' \\
|
|
343
|
+
'--json[Machine-readable JSON output]' \\
|
|
344
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
345
|
+
;;
|
|
346
|
+
explain)
|
|
347
|
+
_arguments \\
|
|
348
|
+
'1:topic:(\$explain_topics)'
|
|
349
|
+
;;
|
|
350
|
+
similarity)
|
|
351
|
+
_arguments \\
|
|
352
|
+
'--against[Compare against multiple texts]:text:' \\
|
|
353
|
+
'--file1[Read text A from file]:file:_files' \\
|
|
354
|
+
'--file2[Read text B from file]:file:_files' \\
|
|
355
|
+
'(-m --model)'{-m,--model}'[Embedding model]:model:(\$models)' \\
|
|
356
|
+
'--dimensions[Output dimensions]:dimensions:' \\
|
|
357
|
+
'--json[Machine-readable JSON output]' \\
|
|
358
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]' \\
|
|
359
|
+
'*:text:'
|
|
360
|
+
;;
|
|
361
|
+
ingest)
|
|
362
|
+
_arguments \\
|
|
363
|
+
'--file[Input file]:file:_files' \\
|
|
364
|
+
'--db[Database name]:database:' \\
|
|
365
|
+
'--collection[Collection name]:collection:' \\
|
|
366
|
+
'--field[Embedding field name]:field:' \\
|
|
367
|
+
'(-m --model)'{-m,--model}'[Embedding model]:model:(\$models)' \\
|
|
368
|
+
'--input-type[Input type]:type:(query document)' \\
|
|
369
|
+
'(-d --dimensions)'{-d,--dimensions}'[Output dimensions]:dimensions:' \\
|
|
370
|
+
'--batch-size[Documents per batch]:size:' \\
|
|
371
|
+
'--text-field[JSON field containing text]:field:' \\
|
|
372
|
+
'--text-column[CSV column to embed]:column:' \\
|
|
373
|
+
'--strict[Abort on first batch error]' \\
|
|
374
|
+
'--dry-run[Validate only, no API calls]' \\
|
|
375
|
+
'--json[Machine-readable JSON output]' \\
|
|
376
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
377
|
+
;;
|
|
378
|
+
completions)
|
|
379
|
+
_arguments \\
|
|
380
|
+
'1:shell:(bash zsh)'
|
|
381
|
+
;;
|
|
382
|
+
esac
|
|
383
|
+
;;
|
|
384
|
+
esac
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
_vai "\$@"
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Show installation instructions for shell completions.
|
|
393
|
+
* @param {string} shell - 'bash' or 'zsh'
|
|
394
|
+
*/
|
|
395
|
+
function showInstallInstructions(shell) {
|
|
396
|
+
console.log('');
|
|
397
|
+
if (shell === 'bash') {
|
|
398
|
+
console.log(` ${pc.bold('Install bash completions:')}`);
|
|
399
|
+
console.log('');
|
|
400
|
+
console.log(` ${pc.cyan('# Add to your ~/.bashrc (or ~/.bash_profile on macOS)')}`);
|
|
401
|
+
console.log(` ${pc.white('vai completions bash >> ~/.bashrc')}`);
|
|
402
|
+
console.log(` ${pc.white('source ~/.bashrc')}`);
|
|
403
|
+
console.log('');
|
|
404
|
+
console.log(` ${pc.cyan('# Or install system-wide (Linux)')}`);
|
|
405
|
+
console.log(` ${pc.white('vai completions bash > /etc/bash_completion.d/vai')}`);
|
|
406
|
+
console.log('');
|
|
407
|
+
console.log(` ${pc.cyan('# Or with Homebrew (macOS)')}`);
|
|
408
|
+
console.log(` ${pc.white('vai completions bash > $(brew --prefix)/etc/bash_completion.d/vai')}`);
|
|
409
|
+
} else {
|
|
410
|
+
console.log(` ${pc.bold('Install zsh completions:')}`);
|
|
411
|
+
console.log('');
|
|
412
|
+
console.log(` ${pc.cyan('# Create completions directory if needed')}`);
|
|
413
|
+
console.log(` ${pc.white('mkdir -p ~/.zsh/completions')}`);
|
|
414
|
+
console.log('');
|
|
415
|
+
console.log(` ${pc.cyan('# Add to fpath in your ~/.zshrc (if not already there)')}`);
|
|
416
|
+
console.log(` ${pc.white('echo \'fpath=(~/.zsh/completions $fpath)\' >> ~/.zshrc')}`);
|
|
417
|
+
console.log(` ${pc.white('echo \'autoload -Uz compinit && compinit\' >> ~/.zshrc')}`);
|
|
418
|
+
console.log('');
|
|
419
|
+
console.log(` ${pc.cyan('# Generate the completion file')}`);
|
|
420
|
+
console.log(` ${pc.white('vai completions zsh > ~/.zsh/completions/_vai')}`);
|
|
421
|
+
console.log(` ${pc.white('source ~/.zshrc')}`);
|
|
422
|
+
console.log('');
|
|
423
|
+
console.log(` ${pc.cyan('# Or install system-wide')}`);
|
|
424
|
+
console.log(` ${pc.white('vai completions zsh > /usr/local/share/zsh/site-functions/_vai')}`);
|
|
425
|
+
}
|
|
426
|
+
console.log('');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Register the completions command on a Commander program.
|
|
431
|
+
* @param {import('commander').Command} program
|
|
432
|
+
*/
|
|
433
|
+
function registerCompletions(program) {
|
|
434
|
+
program
|
|
435
|
+
.command('completions [shell]')
|
|
436
|
+
.description('Generate shell completion scripts (bash or zsh)')
|
|
437
|
+
.action((shell) => {
|
|
438
|
+
if (!shell) {
|
|
439
|
+
console.log('');
|
|
440
|
+
console.log(` ${pc.bold('Usage:')} vai completions ${pc.cyan('<bash|zsh>')}`);
|
|
441
|
+
console.log('');
|
|
442
|
+
console.log(' Outputs a completion script for the specified shell.');
|
|
443
|
+
console.log(' Redirect the output to the appropriate file for your shell.');
|
|
444
|
+
console.log('');
|
|
445
|
+
showInstallInstructions('bash');
|
|
446
|
+
showInstallInstructions('zsh');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const normalized = shell.toLowerCase().trim();
|
|
451
|
+
|
|
452
|
+
if (normalized === 'bash') {
|
|
453
|
+
process.stdout.write(generateBashCompletions());
|
|
454
|
+
} else if (normalized === 'zsh') {
|
|
455
|
+
process.stdout.write(generateZshCompletions());
|
|
456
|
+
} else {
|
|
457
|
+
console.error(` ${pc.red('✗')} Unknown shell: ${shell}. Supported: bash, zsh`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
module.exports = { registerCompletions, generateBashCompletions, generateZshCompletions };
|
package/src/commands/models.js
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { MODEL_CATALOG } = require('../lib/catalog');
|
|
4
|
-
const {
|
|
4
|
+
const { getApiBase } = require('../lib/api');
|
|
5
5
|
const { formatTable } = require('../lib/format');
|
|
6
6
|
const ui = require('../lib/ui');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Shorten dimensions string for compact display.
|
|
10
|
+
* "1024 (default), 256, 512, 2048" → "1024*"
|
|
11
|
+
* "1024" → "1024"
|
|
12
|
+
* "—" → "—"
|
|
13
|
+
* @param {string} dims
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function compactDimensions(dims) {
|
|
17
|
+
if (dims === '—') return dims;
|
|
18
|
+
const match = dims.match(/^(\d+)\s*\(default\)/);
|
|
19
|
+
if (match) return match[1] + '*';
|
|
20
|
+
return dims;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Shorten price string for compact display.
|
|
25
|
+
* "$0.12/1M tokens" → "$0.12/1M"
|
|
26
|
+
* "$0.12/M + $0.60/B px" → "$0.12/M+$0.60/Bpx"
|
|
27
|
+
* @param {string} price
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function compactPrice(price) {
|
|
31
|
+
return price.replace('/1M tokens', '/1M').replace(' + ', '+').replace('/B px', '/Bpx');
|
|
32
|
+
}
|
|
33
|
+
|
|
8
34
|
/**
|
|
9
35
|
* Register the models command on a Commander program.
|
|
10
36
|
* @param {import('commander').Command} program
|
|
@@ -14,6 +40,7 @@ function registerModels(program) {
|
|
|
14
40
|
.command('models')
|
|
15
41
|
.description('List available Voyage AI models')
|
|
16
42
|
.option('-t, --type <type>', 'Filter by type: embedding, reranking, or all', 'all')
|
|
43
|
+
.option('-w, --wide', 'Wide output (show all columns untruncated)')
|
|
17
44
|
.option('--json', 'Machine-readable JSON output')
|
|
18
45
|
.option('-q, --quiet', 'Suppress non-essential output')
|
|
19
46
|
.action((opts) => {
|
|
@@ -33,28 +60,49 @@ function registerModels(program) {
|
|
|
33
60
|
return;
|
|
34
61
|
}
|
|
35
62
|
|
|
63
|
+
const apiBase = getApiBase();
|
|
64
|
+
|
|
36
65
|
if (!opts.quiet) {
|
|
37
66
|
console.log(ui.bold('Voyage AI Models'));
|
|
38
|
-
console.log(ui.dim(`(via
|
|
67
|
+
console.log(ui.dim(`(via ${apiBase})`));
|
|
39
68
|
console.log('');
|
|
40
69
|
}
|
|
41
70
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
if (opts.wide) {
|
|
72
|
+
// Full table with all details
|
|
73
|
+
const headers = ['Model', 'Type', 'Context', 'Dimensions', 'Price', 'Best For'];
|
|
74
|
+
const rows = models.map(m => {
|
|
75
|
+
const name = ui.cyan(m.name);
|
|
76
|
+
const type = m.type === 'embedding' ? ui.green(m.type) : ui.yellow(m.type);
|
|
77
|
+
const price = ui.dim(m.price);
|
|
78
|
+
return [name, type, m.context, m.dimensions, price, m.bestFor];
|
|
79
|
+
});
|
|
80
|
+
const boldHeaders = headers.map(h => ui.bold(h));
|
|
81
|
+
console.log(formatTable(boldHeaders, rows));
|
|
82
|
+
} else {
|
|
83
|
+
// Compact table — fits in 80 cols
|
|
84
|
+
const headers = ['Model', 'Type', 'Dims', 'Price', 'Use Case'];
|
|
85
|
+
const rows = models.map(m => {
|
|
86
|
+
const name = ui.cyan(m.name);
|
|
87
|
+
const type = m.type === 'embedding' ? ui.green('embed') : ui.yellow('rerank');
|
|
88
|
+
const dims = compactDimensions(m.dimensions);
|
|
89
|
+
const price = ui.dim(compactPrice(m.price));
|
|
90
|
+
return [name, type, dims, price, m.shortFor || m.bestFor];
|
|
91
|
+
});
|
|
92
|
+
const boldHeaders = headers.map(h => ui.bold(h));
|
|
93
|
+
console.log(formatTable(boldHeaders, rows));
|
|
94
|
+
}
|
|
53
95
|
|
|
54
96
|
if (!opts.quiet) {
|
|
55
97
|
console.log('');
|
|
98
|
+
if (!opts.wide) {
|
|
99
|
+
console.log(ui.dim('* = also supports 256, 512, 2048 dimensions'));
|
|
100
|
+
}
|
|
56
101
|
console.log(ui.dim('Free tier: 200M tokens (most models), 50M (domain-specific)'));
|
|
57
102
|
console.log(ui.dim('All 4-series models share the same embedding space.'));
|
|
103
|
+
if (!opts.wide) {
|
|
104
|
+
console.log(ui.dim('Use --wide for full details.'));
|
|
105
|
+
}
|
|
58
106
|
}
|
|
59
107
|
});
|
|
60
108
|
}
|
package/src/commands/ping.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { getApiBase, requireApiKey } = require('../lib/api');
|
|
4
4
|
const ui = require('../lib/ui');
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -28,6 +28,7 @@ function registerPing(program) {
|
|
|
28
28
|
const useColor = !opts.json;
|
|
29
29
|
const useSpinner = useColor && !opts.quiet;
|
|
30
30
|
|
|
31
|
+
const apiBase = getApiBase();
|
|
31
32
|
const model = 'voyage-4-lite';
|
|
32
33
|
const startTime = Date.now();
|
|
33
34
|
|
|
@@ -38,7 +39,7 @@ function registerPing(program) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
try {
|
|
41
|
-
const response = await fetch(`${
|
|
42
|
+
const response = await fetch(`${apiBase}/embeddings`, {
|
|
42
43
|
method: 'POST',
|
|
43
44
|
headers: {
|
|
44
45
|
'Content-Type': 'application/json',
|
|
@@ -83,7 +84,7 @@ function registerPing(program) {
|
|
|
83
84
|
const dims = data.data && data.data[0] ? data.data[0].embedding.length : 'unknown';
|
|
84
85
|
const tokens = data.usage ? data.usage.total_tokens : 'unknown';
|
|
85
86
|
|
|
86
|
-
results.voyage = { ok: true, elapsed, model, dimensions: dims, tokens, endpoint:
|
|
87
|
+
results.voyage = { ok: true, elapsed, model, dimensions: dims, tokens, endpoint: apiBase };
|
|
87
88
|
|
|
88
89
|
if (spin) spin.stop();
|
|
89
90
|
|
|
@@ -93,7 +94,7 @@ function registerPing(program) {
|
|
|
93
94
|
console.log(`ok ${elapsed}ms`);
|
|
94
95
|
} else {
|
|
95
96
|
console.log(ui.success(`Connected to Voyage AI API ${ui.dim('(' + elapsed + 'ms)')}`));
|
|
96
|
-
console.log(ui.label('Endpoint',
|
|
97
|
+
console.log(ui.label('Endpoint', apiBase));
|
|
97
98
|
console.log(ui.label('Model', model));
|
|
98
99
|
console.log(ui.label('Dimensions', String(dims)));
|
|
99
100
|
console.log(ui.label('Tokens', String(tokens)));
|
package/src/lib/api.js
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const ATLAS_API_BASE = 'https://ai.mongodb.com/v1';
|
|
4
|
+
const VOYAGE_API_BASE = 'https://api.voyageai.com/v1';
|
|
4
5
|
const MAX_RETRIES = 3;
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the API base URL.
|
|
9
|
+
* Priority: VOYAGE_API_BASE env → config baseUrl → auto-detect from key prefix.
|
|
10
|
+
* Keys starting with 'pa-' that work on Voyage platform use VOYAGE_API_BASE.
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function getApiBase() {
|
|
14
|
+
const { getConfigValue } = require('./config');
|
|
15
|
+
|
|
16
|
+
// Explicit override wins
|
|
17
|
+
const envBase = process.env.VOYAGE_API_BASE;
|
|
18
|
+
if (envBase) return envBase.replace(/\/+$/, '');
|
|
19
|
+
|
|
20
|
+
const configBase = getConfigValue('baseUrl');
|
|
21
|
+
if (configBase) return configBase.replace(/\/+$/, '');
|
|
22
|
+
|
|
23
|
+
// Default to Atlas endpoint
|
|
24
|
+
return ATLAS_API_BASE;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Legacy export for backward compat
|
|
28
|
+
const API_BASE = ATLAS_API_BASE;
|
|
29
|
+
|
|
6
30
|
/**
|
|
7
31
|
* Get the Voyage API key or exit with a helpful error.
|
|
8
32
|
* Checks: env var → config file.
|
|
@@ -18,6 +42,7 @@ function requireApiKey() {
|
|
|
18
42
|
console.error('Option 2: vai config set api-key <your-key>');
|
|
19
43
|
console.error('');
|
|
20
44
|
console.error('Get one from MongoDB Atlas → AI Models → Create model API key');
|
|
45
|
+
console.error(' or Voyage AI platform → Dashboard → API Keys');
|
|
21
46
|
process.exit(1);
|
|
22
47
|
}
|
|
23
48
|
return key;
|
|
@@ -40,7 +65,8 @@ function sleep(ms) {
|
|
|
40
65
|
*/
|
|
41
66
|
async function apiRequest(endpoint, body) {
|
|
42
67
|
const apiKey = requireApiKey();
|
|
43
|
-
const
|
|
68
|
+
const base = getApiBase();
|
|
69
|
+
const url = `${base}${endpoint}`;
|
|
44
70
|
|
|
45
71
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
46
72
|
const response = await fetch(url, {
|
|
@@ -69,6 +95,23 @@ async function apiRequest(endpoint, body) {
|
|
|
69
95
|
errorDetail = await response.text();
|
|
70
96
|
}
|
|
71
97
|
console.error(`API Error (${response.status}): ${errorDetail}`);
|
|
98
|
+
|
|
99
|
+
// Help users diagnose endpoint mismatch
|
|
100
|
+
if (response.status === 403 && base === ATLAS_API_BASE) {
|
|
101
|
+
console.error('');
|
|
102
|
+
console.error('Hint: 403 on ai.mongodb.com often means your key is for the Voyage AI');
|
|
103
|
+
console.error('platform, not MongoDB Atlas. Try switching the base URL:');
|
|
104
|
+
console.error('');
|
|
105
|
+
console.error(' vai config set base-url https://api.voyageai.com/v1/');
|
|
106
|
+
console.error('');
|
|
107
|
+
console.error('Or set VOYAGE_API_BASE=https://api.voyageai.com/v1/ in your environment.');
|
|
108
|
+
} else if (response.status === 401 && base === VOYAGE_API_BASE) {
|
|
109
|
+
console.error('');
|
|
110
|
+
console.error('Hint: 401 on api.voyageai.com may mean your key is an Atlas AI key.');
|
|
111
|
+
console.error('Try switching back:');
|
|
112
|
+
console.error('');
|
|
113
|
+
console.error(' vai config set base-url https://ai.mongodb.com/v1/');
|
|
114
|
+
}
|
|
72
115
|
process.exit(1);
|
|
73
116
|
}
|
|
74
117
|
|
|
@@ -105,6 +148,9 @@ async function generateEmbeddings(texts, options = {}) {
|
|
|
105
148
|
|
|
106
149
|
module.exports = {
|
|
107
150
|
API_BASE,
|
|
151
|
+
ATLAS_API_BASE,
|
|
152
|
+
VOYAGE_API_BASE,
|
|
153
|
+
getApiBase,
|
|
108
154
|
requireApiKey,
|
|
109
155
|
apiRequest,
|
|
110
156
|
generateEmbeddings,
|
package/src/lib/catalog.js
CHANGED
|
@@ -24,16 +24,16 @@ function getDefaultDimensions() {
|
|
|
24
24
|
|
|
25
25
|
/** @type {Array<{name: string, type: string, context: string, dimensions: string, price: string, bestFor: string}>} */
|
|
26
26
|
const MODEL_CATALOG = [
|
|
27
|
-
{ name: 'voyage-4-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/1M tokens', bestFor: 'Best quality, multilingual' },
|
|
28
|
-
{ name: 'voyage-4', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.06/1M tokens', bestFor: 'Balanced quality/perf' },
|
|
29
|
-
{ name: 'voyage-4-lite', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.02/1M tokens', bestFor: 'Lowest cost' },
|
|
30
|
-
{ name: 'voyage-code-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', bestFor: 'Code retrieval' },
|
|
31
|
-
{ name: 'voyage-finance-2', type: 'embedding', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Finance' },
|
|
32
|
-
{ name: 'voyage-law-2', type: 'embedding', context: '16K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Legal' },
|
|
33
|
-
{ name: 'voyage-context-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', bestFor: 'Contextualized chunks' },
|
|
34
|
-
{ name: 'voyage-multimodal-3.5', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/M + $0.60/B px', bestFor: 'Text + images + video' },
|
|
35
|
-
{ name: 'rerank-2.5', type: 'reranking', context: '32K', dimensions: '—', price: '$0.05/1M tokens', bestFor: 'Best quality reranking' },
|
|
36
|
-
{ name: 'rerank-2.5-lite', type: 'reranking', context: '32K', dimensions: '—', price: '$0.02/1M tokens', bestFor: 'Fast reranking' },
|
|
27
|
+
{ name: 'voyage-4-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/1M tokens', bestFor: 'Best quality, multilingual', shortFor: 'Best quality' },
|
|
28
|
+
{ name: 'voyage-4', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.06/1M tokens', bestFor: 'Balanced quality/perf', shortFor: 'Balanced' },
|
|
29
|
+
{ name: 'voyage-4-lite', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.02/1M tokens', bestFor: 'Lowest cost', shortFor: 'Budget' },
|
|
30
|
+
{ name: 'voyage-code-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', bestFor: 'Code retrieval', shortFor: 'Code' },
|
|
31
|
+
{ name: 'voyage-finance-2', type: 'embedding', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Finance', shortFor: 'Finance' },
|
|
32
|
+
{ name: 'voyage-law-2', type: 'embedding', context: '16K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Legal', shortFor: 'Legal' },
|
|
33
|
+
{ name: 'voyage-context-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', bestFor: 'Contextualized chunks', shortFor: 'Context chunks' },
|
|
34
|
+
{ name: 'voyage-multimodal-3.5', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/M + $0.60/B px', bestFor: 'Text + images + video', shortFor: 'Multimodal' },
|
|
35
|
+
{ name: 'rerank-2.5', type: 'reranking', context: '32K', dimensions: '—', price: '$0.05/1M tokens', bestFor: 'Best quality reranking', shortFor: 'Best reranker' },
|
|
36
|
+
{ name: 'rerank-2.5-lite', type: 'reranking', context: '32K', dimensions: '—', price: '$0.02/1M tokens', bestFor: 'Fast reranking', shortFor: 'Fast reranker' },
|
|
37
37
|
];
|
|
38
38
|
|
|
39
39
|
module.exports = {
|
package/src/lib/config.js
CHANGED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { Command } = require('commander');
|
|
6
|
+
const { registerCompletions, generateBashCompletions, generateZshCompletions } = require('../../src/commands/completions');
|
|
7
|
+
|
|
8
|
+
describe('completions command', () => {
|
|
9
|
+
let originalLog;
|
|
10
|
+
let originalWrite;
|
|
11
|
+
let originalError;
|
|
12
|
+
let originalExit;
|
|
13
|
+
let output;
|
|
14
|
+
let stdoutOutput;
|
|
15
|
+
let stderrOutput;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
originalLog = console.log;
|
|
19
|
+
originalWrite = process.stdout.write;
|
|
20
|
+
originalError = console.error;
|
|
21
|
+
originalExit = process.exit;
|
|
22
|
+
output = [];
|
|
23
|
+
stdoutOutput = [];
|
|
24
|
+
stderrOutput = [];
|
|
25
|
+
console.log = (...args) => output.push(args.join(' '));
|
|
26
|
+
process.stdout.write = (data) => { stdoutOutput.push(data); return true; };
|
|
27
|
+
console.error = (...args) => stderrOutput.push(args.join(' '));
|
|
28
|
+
process.exit = (code) => { throw new Error(`EXIT_${code}`); };
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
console.log = originalLog;
|
|
33
|
+
process.stdout.write = originalWrite;
|
|
34
|
+
console.error = originalError;
|
|
35
|
+
process.exit = originalExit;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('registers correctly on a program', () => {
|
|
39
|
+
const program = new Command();
|
|
40
|
+
registerCompletions(program);
|
|
41
|
+
const cmd = program.commands.find(c => c.name() === 'completions');
|
|
42
|
+
assert.ok(cmd, 'completions command should be registered');
|
|
43
|
+
assert.ok(cmd.description().includes('completion'), 'should have a description about completions');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('shows usage when called without shell argument', async () => {
|
|
47
|
+
const program = new Command();
|
|
48
|
+
program.exitOverride();
|
|
49
|
+
registerCompletions(program);
|
|
50
|
+
|
|
51
|
+
await program.parseAsync(['node', 'test', 'completions']);
|
|
52
|
+
|
|
53
|
+
const combined = output.join('\n');
|
|
54
|
+
assert.ok(combined.includes('bash'), 'should mention bash');
|
|
55
|
+
assert.ok(combined.includes('zsh'), 'should mention zsh');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('outputs bash completion script', async () => {
|
|
59
|
+
const program = new Command();
|
|
60
|
+
program.exitOverride();
|
|
61
|
+
registerCompletions(program);
|
|
62
|
+
|
|
63
|
+
await program.parseAsync(['node', 'test', 'completions', 'bash']);
|
|
64
|
+
|
|
65
|
+
const combined = stdoutOutput.join('');
|
|
66
|
+
assert.ok(combined.includes('_vai_completions'), 'should contain bash completion function');
|
|
67
|
+
assert.ok(combined.includes('complete -F _vai_completions vai'), 'should register completion');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('outputs zsh completion script', async () => {
|
|
71
|
+
const program = new Command();
|
|
72
|
+
program.exitOverride();
|
|
73
|
+
registerCompletions(program);
|
|
74
|
+
|
|
75
|
+
await program.parseAsync(['node', 'test', 'completions', 'zsh']);
|
|
76
|
+
|
|
77
|
+
const combined = stdoutOutput.join('');
|
|
78
|
+
assert.ok(combined.includes('#compdef vai'), 'should contain zsh compdef header');
|
|
79
|
+
assert.ok(combined.includes('_vai'), 'should contain zsh completion function');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rejects unknown shell', async () => {
|
|
83
|
+
const program = new Command();
|
|
84
|
+
program.exitOverride();
|
|
85
|
+
registerCompletions(program);
|
|
86
|
+
|
|
87
|
+
await assert.rejects(
|
|
88
|
+
() => program.parseAsync(['node', 'test', 'completions', 'fish']),
|
|
89
|
+
/EXIT_1/,
|
|
90
|
+
'should exit with code 1 for unsupported shell'
|
|
91
|
+
);
|
|
92
|
+
const combined = stderrOutput.join('\n');
|
|
93
|
+
assert.ok(combined.includes('fish'), 'should mention the unknown shell name');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('generateBashCompletions', () => {
|
|
98
|
+
it('includes all 14 commands (including completions)', () => {
|
|
99
|
+
const script = generateBashCompletions();
|
|
100
|
+
const commands = ['embed', 'rerank', 'store', 'search', 'index', 'models', 'ping', 'config', 'demo', 'explain', 'similarity', 'ingest', 'completions', 'help'];
|
|
101
|
+
for (const cmd of commands) {
|
|
102
|
+
assert.ok(script.includes(cmd), `should include command: ${cmd}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('includes model completions', () => {
|
|
107
|
+
const script = generateBashCompletions();
|
|
108
|
+
assert.ok(script.includes('voyage-4-large'), 'should include voyage-4-large model');
|
|
109
|
+
assert.ok(script.includes('rerank-2.5'), 'should include rerank-2.5 model');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('includes flag completions for embed', () => {
|
|
113
|
+
const script = generateBashCompletions();
|
|
114
|
+
assert.ok(script.includes('--model'), 'should include --model flag');
|
|
115
|
+
assert.ok(script.includes('--dimensions'), 'should include --dimensions flag');
|
|
116
|
+
assert.ok(script.includes('--input-type'), 'should include --input-type flag');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('includes index subcommands', () => {
|
|
120
|
+
const script = generateBashCompletions();
|
|
121
|
+
assert.ok(script.includes('create list delete'), 'should include index subcommands');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('includes config subcommands', () => {
|
|
125
|
+
const script = generateBashCompletions();
|
|
126
|
+
assert.ok(script.includes('set get delete path reset'), 'should include config subcommands');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('includes input-type values', () => {
|
|
130
|
+
const script = generateBashCompletions();
|
|
131
|
+
assert.ok(script.includes('query document'), 'should include input-type values');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('generateZshCompletions', () => {
|
|
136
|
+
it('includes compdef header', () => {
|
|
137
|
+
const script = generateZshCompletions();
|
|
138
|
+
assert.ok(script.startsWith('#compdef vai'), 'should start with #compdef vai');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('includes all commands with descriptions', () => {
|
|
142
|
+
const script = generateZshCompletions();
|
|
143
|
+
const commands = ['embed', 'rerank', 'store', 'search', 'index', 'models', 'ping', 'config', 'demo', 'explain', 'similarity', 'ingest', 'completions'];
|
|
144
|
+
for (const cmd of commands) {
|
|
145
|
+
assert.ok(script.includes(`'${cmd}:`), `should include command with description: ${cmd}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('includes model names', () => {
|
|
150
|
+
const script = generateZshCompletions();
|
|
151
|
+
assert.ok(script.includes('voyage-4-large'), 'should include voyage-4-large model');
|
|
152
|
+
assert.ok(script.includes('voyage-code-3'), 'should include voyage-code-3 model');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('includes explain topics', () => {
|
|
156
|
+
const script = generateZshCompletions();
|
|
157
|
+
assert.ok(script.includes('embeddings'), 'should include embeddings topic');
|
|
158
|
+
assert.ok(script.includes('cosine-similarity'), 'should include cosine-similarity topic');
|
|
159
|
+
assert.ok(script.includes('batch-processing'), 'should include batch-processing topic');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('includes file completion for --file flags', () => {
|
|
163
|
+
const script = generateZshCompletions();
|
|
164
|
+
assert.ok(script.includes('_files'), 'should use _files completion');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -105,8 +105,17 @@ describe('ping command', () => {
|
|
|
105
105
|
assert.ok(combined.includes('Authentication failed'), 'Should show auth error');
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
it('exits when VOYAGE_API_KEY is not set', async () => {
|
|
108
|
+
it('exits when VOYAGE_API_KEY is not set and no config', async () => {
|
|
109
109
|
delete process.env.VOYAGE_API_KEY;
|
|
110
|
+
// Mock config to return nothing so the key isn't found in ~/.vai/config.json
|
|
111
|
+
delete require.cache[require.resolve('../../src/lib/config')];
|
|
112
|
+
delete require.cache[require.resolve('../../src/lib/api')];
|
|
113
|
+
delete require.cache[require.resolve('../../src/commands/ping')];
|
|
114
|
+
const config = require('../../src/lib/config');
|
|
115
|
+
const origGetConfigValue = config.getConfigValue;
|
|
116
|
+
config.getConfigValue = () => undefined;
|
|
117
|
+
|
|
118
|
+
const { registerPing: registerPingFresh } = require('../../src/commands/ping');
|
|
110
119
|
|
|
111
120
|
let exitCode = null;
|
|
112
121
|
process.exit = (code) => {
|
|
@@ -116,16 +125,20 @@ describe('ping command', () => {
|
|
|
116
125
|
|
|
117
126
|
const program = new Command();
|
|
118
127
|
program.exitOverride();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
registerPingFresh(program);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await assert.rejects(
|
|
132
|
+
() => program.parseAsync(['node', 'test', 'ping']),
|
|
133
|
+
/process\.exit called/
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
assert.equal(exitCode, 1);
|
|
137
|
+
const combined = errorOutput.join('\n');
|
|
138
|
+
assert.ok(combined.includes('VOYAGE_API_KEY'), 'Should mention missing key');
|
|
139
|
+
} finally {
|
|
140
|
+
config.getConfigValue = origGetConfigValue;
|
|
141
|
+
}
|
|
129
142
|
});
|
|
130
143
|
|
|
131
144
|
it('outputs JSON when --json flag is used', async () => {
|
package/test/lib/api.test.js
CHANGED
|
@@ -25,8 +25,13 @@ describe('api', () => {
|
|
|
25
25
|
describe('requireApiKey', () => {
|
|
26
26
|
it('throws/exits when VOYAGE_API_KEY is not set', () => {
|
|
27
27
|
delete process.env.VOYAGE_API_KEY;
|
|
28
|
-
// Re-require to get fresh
|
|
28
|
+
// Re-require to get fresh modules (clear config cache too)
|
|
29
29
|
delete require.cache[require.resolve('../../src/lib/api')];
|
|
30
|
+
delete require.cache[require.resolve('../../src/lib/config')];
|
|
31
|
+
const config = require('../../src/lib/config');
|
|
32
|
+
const originalGetConfigValue = config.getConfigValue;
|
|
33
|
+
config.getConfigValue = () => undefined;
|
|
34
|
+
|
|
30
35
|
const { requireApiKey } = require('../../src/lib/api');
|
|
31
36
|
|
|
32
37
|
let exitCode = null;
|
|
@@ -35,8 +40,12 @@ describe('api', () => {
|
|
|
35
40
|
throw new Error('process.exit called');
|
|
36
41
|
};
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
try {
|
|
44
|
+
assert.throws(() => requireApiKey(), /process\.exit called/);
|
|
45
|
+
assert.equal(exitCode, 1);
|
|
46
|
+
} finally {
|
|
47
|
+
config.getConfigValue = originalGetConfigValue;
|
|
48
|
+
}
|
|
40
49
|
});
|
|
41
50
|
|
|
42
51
|
it('returns key when VOYAGE_API_KEY is set', () => {
|