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.
@@ -0,0 +1,21 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version: [18, 20, 22]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: ${{ matrix.node-version }}
20
+ - run: npm ci
21
+ - run: npm test
package/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to voyageai-cli are documented here.
4
+
5
+ Format based on [Keep a Changelog](https://keepachangelog.com/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+ - `vai demo` — Interactive guided walkthrough of all features
11
+ - ASCII banner when running `vai` with no arguments
12
+ - CONTRIBUTING.md for open-source contributors
13
+ - This changelog
14
+
15
+ ## [1.1.0] - 2026-02-03
16
+
17
+ ### Added
18
+ - `vai config` — Persistent config management (`~/.vai/config.json`)
19
+ - `set`, `get`, `delete`, `path`, `reset` subcommands
20
+ - Secrets masked in output, config file chmod 600
21
+ - `--stdin` flag for secure key input (avoids shell history)
22
+ - `vai ping` — Test API and MongoDB connectivity
23
+ - `.env` file support via dotenv
24
+ - Colored output with picocolors (green ✓, red ✗, score-based colors)
25
+ - Animated spinners on all network operations
26
+ - npm update notifier (checks daily, non-blocking)
27
+ - GitHub Actions CI (Node 18, 20, 22)
28
+ - README badges (CI, npm, license, node version)
29
+ - Credential priority chain: env var → .env → config file
30
+ - Security documentation in README
31
+
32
+ ### Fixed
33
+ - `ping` command now falls back to config file for API key and MongoDB URI
34
+ - `rerank` endpoint corrected from `/v1/reranking` to `/v1/rerank`
35
+ - `index create` parseInt handling for dimensions (was producing NaN)
36
+
37
+ ## [1.0.0] - 2026-02-03
38
+
39
+ ### Added
40
+ - `vai embed` — Generate embeddings (text, file, stdin, bulk)
41
+ - `vai rerank` — Rerank documents with relevance scoring
42
+ - `vai store` — Embed and insert into MongoDB Atlas (single + batch JSONL)
43
+ - `vai search` — $vectorSearch with pre-filter support
44
+ - `vai index` — Create, list, delete Atlas Vector Search indexes
45
+ - `vai models` — List available Voyage AI models with pricing
46
+ - REST API integration with `https://ai.mongodb.com/v1/`
47
+ - MongoDB Atlas Vector Search integration
48
+ - API retry on 429 with exponential backoff
49
+ - `--json` and `--quiet` flags on all commands
50
+ - 50+ unit tests
@@ -0,0 +1,81 @@
1
+ # Contributing to voyageai-cli
2
+
3
+ Thanks for your interest in contributing! Here's how to get started.
4
+
5
+ ## Development Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/mrlynn/voyageai-cli.git
9
+ cd voyageai-cli
10
+ npm install
11
+ npm link # makes `vai` available globally for testing
12
+ ```
13
+
14
+ ## Running Tests
15
+
16
+ ```bash
17
+ npm test
18
+ ```
19
+
20
+ Tests use Node.js built-in test runner (`node:test`). No external test framework needed.
21
+
22
+ ## Project Structure
23
+
24
+ ```
25
+ src/
26
+ ├── cli.js # Entry point
27
+ ├── commands/ # One file per command
28
+ │ ├── embed.js
29
+ │ ├── rerank.js
30
+ │ ├── store.js
31
+ │ ├── search.js
32
+ │ ├── index.js
33
+ │ ├── models.js
34
+ │ ├── ping.js
35
+ │ ├── config.js
36
+ │ └── demo.js
37
+ └── lib/ # Shared utilities
38
+ ├── api.js # Voyage AI API client
39
+ ├── mongo.js # MongoDB connection
40
+ ├── catalog.js # Model catalog
41
+ ├── config.js # Config file management
42
+ ├── format.js # Table formatting
43
+ ├── input.js # Text input resolution
44
+ ├── ui.js # Colors, spinners, output helpers
45
+ └── banner.js # ASCII banner
46
+ test/
47
+ ├── commands/ # Command tests
48
+ └── lib/ # Library tests
49
+ ```
50
+
51
+ ## Adding a New Command
52
+
53
+ 1. Create `src/commands/mycommand.js` exporting a `registerMyCommand(program)` function
54
+ 2. Register it in `src/cli.js`
55
+ 3. Add tests in `test/commands/mycommand.test.js`
56
+ 4. Update README.md with usage examples
57
+
58
+ ## Code Style
59
+
60
+ - CommonJS (`require`/`module.exports`)
61
+ - `'use strict';` at the top of every file
62
+ - JSDoc comments on exported functions
63
+ - `parseInt(x, 10)` — always include radix
64
+ - Errors go to stderr (`console.error`)
65
+ - Support `--json` and `--quiet` flags on all commands
66
+ - No colors or spinners in `--json` mode
67
+
68
+ ## Pull Requests
69
+
70
+ - Create a feature branch from `main`
71
+ - Include tests for new functionality
72
+ - Run `npm test` before submitting
73
+ - Write clear commit messages
74
+
75
+ ## Reporting Issues
76
+
77
+ Open an issue at https://github.com/mrlynn/voyageai-cli/issues with:
78
+ - Node.js version (`node --version`)
79
+ - OS and version
80
+ - Steps to reproduce
81
+ - Expected vs actual behavior
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # voyageai-cli
2
2
 
3
+ [![CI](https://github.com/mrlynn/voyageai-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/mrlynn/voyageai-cli/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/voyageai-cli.svg)](https://www.npmjs.com/package/voyageai-cli) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Node.js](https://img.shields.io/node/v/voyageai-cli.svg)](https://nodejs.org)
4
+
3
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.
4
6
 
5
7
  Generate embeddings, rerank search results, store vectors in Atlas, and run semantic search — all from the command line.
@@ -110,6 +112,20 @@ vai index list --db myapp --collection docs
110
112
  vai index delete --db myapp --collection docs --index-name my_index
111
113
  ```
112
114
 
115
+ ### `vai ping` — Test API connectivity
116
+
117
+ ```bash
118
+ # Test Voyage AI API
119
+ vai ping
120
+
121
+ # Also tests MongoDB if MONGODB_URI is set
122
+ export MONGODB_URI="mongodb+srv://user:pass@cluster.mongodb.net/"
123
+ vai ping
124
+
125
+ # JSON output
126
+ vai ping --json
127
+ ```
128
+
113
129
  ### `vai models` — List available models
114
130
 
115
131
  ```bash
@@ -154,8 +170,41 @@ vai rerank --query "how does cloud database work" \
154
170
 
155
171
  | Variable | Required For | Description |
156
172
  |----------|-------------|-------------|
157
- | `VOYAGE_API_KEY` | embed, rerank, store, search | [Model API key](https://www.mongodb.com/docs/voyageai/management/api-keys/) from MongoDB Atlas |
158
- | `MONGODB_URI` | store, search, index | MongoDB Atlas connection string |
173
+ | `VOYAGE_API_KEY` | embed, rerank, store, search, ping | [Model API key](https://www.mongodb.com/docs/voyageai/management/api-keys/) from MongoDB Atlas |
174
+ | `MONGODB_URI` | store, search, index, ping (optional) | MongoDB Atlas connection string |
175
+
176
+ Credentials are resolved in this order (highest priority first):
177
+
178
+ 1. **Environment variables** (`export VOYAGE_API_KEY=...`)
179
+ 2. **`.env` file** in your working directory
180
+ 3. **Config file** (`~/.vai/config.json` via `vai config set`)
181
+
182
+ You can also create a `.env` file in your project directory instead of exporting variables:
183
+
184
+ ```
185
+ VOYAGE_API_KEY=your-key
186
+ MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/
187
+ ```
188
+
189
+ > ⚠️ **Add `.env` to your `.gitignore`** to avoid accidentally committing secrets.
190
+
191
+ Or use the built-in config store:
192
+
193
+ ```bash
194
+ # Pipe to avoid key appearing in shell history
195
+ echo "your-key" | vai config set api-key --stdin
196
+ vai config set mongodb-uri "mongodb+srv://user:pass@cluster.mongodb.net/"
197
+
198
+ # Verify (secrets are masked)
199
+ vai config get
200
+ ```
201
+
202
+ ### Security
203
+
204
+ - Config file (`~/.vai/config.json`) is created with `600` permissions (owner read/write only)
205
+ - Secrets are always masked in `vai config get` output
206
+ - Use `echo "key" | vai config set api-key --stdin` or `vai config set api-key --stdin < keyfile` to avoid shell history exposure
207
+ - The config file stores credentials in plaintext (similar to `~/.aws/credentials` and `~/.npmrc`) — protect your home directory accordingly
159
208
 
160
209
  ## Global Flags
161
210
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voyageai-cli",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search",
5
5
  "bin": {
6
6
  "vai": "./src/cli.js"
@@ -27,11 +27,18 @@
27
27
  "bugs": {
28
28
  "url": "https://github.com/mrlynn/voyageai-cli/issues"
29
29
  },
30
+ "scripts": {
31
+ "test": "node --test test/**/*.test.js"
32
+ },
30
33
  "engines": {
31
34
  "node": ">=18.0.0"
32
35
  },
33
36
  "dependencies": {
34
37
  "commander": "^12.0.0",
35
- "mongodb": "^6.0.0"
38
+ "dotenv": "^17.2.3",
39
+ "mongodb": "^6.0.0",
40
+ "ora": "^9.1.0",
41
+ "picocolors": "^1.1.1",
42
+ "update-notifier": "^7.3.1"
36
43
  }
37
44
  }
package/src/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ require('dotenv').config({ quiet: true });
5
+
4
6
  const { program } = require('commander');
5
7
  const { registerEmbed } = require('./commands/embed');
6
8
  const { registerRerank } = require('./commands/rerank');
@@ -8,11 +10,15 @@ const { registerStore } = require('./commands/store');
8
10
  const { registerSearch } = require('./commands/search');
9
11
  const { registerIndex } = require('./commands/index');
10
12
  const { registerModels } = require('./commands/models');
13
+ const { registerPing } = require('./commands/ping');
14
+ const { registerConfig } = require('./commands/config');
15
+ const { registerDemo } = require('./commands/demo');
16
+ const { showBanner, showQuickStart } = require('./lib/banner');
11
17
 
12
18
  program
13
19
  .name('vai')
14
20
  .description('Voyage AI embeddings, reranking, and Atlas Vector Search CLI')
15
- .version('1.0.0');
21
+ .version('1.1.0');
16
22
 
17
23
  registerEmbed(program);
18
24
  registerRerank(program);
@@ -20,5 +26,16 @@ registerStore(program);
20
26
  registerSearch(program);
21
27
  registerIndex(program);
22
28
  registerModels(program);
29
+ registerPing(program);
30
+ registerConfig(program);
31
+ registerDemo(program);
32
+
33
+ // If no args (just `vai`), show banner + quick start + help
34
+ if (process.argv.length <= 2) {
35
+ showBanner();
36
+ showQuickStart();
37
+ program.outputHelp();
38
+ process.exit(0);
39
+ }
23
40
 
24
41
  program.parse();
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const {
5
+ CONFIG_PATH,
6
+ KEY_MAP,
7
+ SECRET_KEYS,
8
+ loadConfig,
9
+ saveConfig,
10
+ getConfigValue,
11
+ setConfigValue,
12
+ deleteConfigValue,
13
+ maskSecret,
14
+ } = require('../lib/config');
15
+ const ui = require('../lib/ui');
16
+
17
+ const VALID_KEYS = Object.keys(KEY_MAP);
18
+
19
+ /**
20
+ * Register the config command on a Commander program.
21
+ * @param {import('commander').Command} program
22
+ */
23
+ function registerConfig(program) {
24
+ const configCmd = program
25
+ .command('config')
26
+ .description('Manage persistent configuration (~/.vai/config.json)');
27
+
28
+ // ── config set <key> [value] ──
29
+ configCmd
30
+ .command('set <key> [value]')
31
+ .description('Set a config value (omit value to read from stdin)')
32
+ .option('--stdin', 'Read value from stdin (avoids shell history exposure)')
33
+ .action(async (key, value, opts) => {
34
+ const internalKey = KEY_MAP[key];
35
+ if (!internalKey) {
36
+ console.error(ui.error(`Unknown config key "${ui.cyan(key)}".`));
37
+ console.error(`Valid keys: ${VALID_KEYS.join(', ')}`);
38
+ process.exit(1);
39
+ }
40
+
41
+ // Read from stdin if no value provided or --stdin flag
42
+ if (!value || opts.stdin) {
43
+ if (process.stdin.isTTY && !value) {
44
+ // Interactive: prompt without echo for secrets
45
+ const isSecret = SECRET_KEYS.has(internalKey);
46
+ if (isSecret) {
47
+ process.stderr.write(`Enter ${key}: `);
48
+ } else {
49
+ process.stderr.write(`Enter ${key}: `);
50
+ }
51
+ }
52
+ if (!value) {
53
+ const chunks = [];
54
+ for await (const chunk of process.stdin) {
55
+ chunks.push(chunk);
56
+ }
57
+ value = Buffer.concat(chunks).toString('utf-8').trim();
58
+ }
59
+ }
60
+
61
+ if (!value) {
62
+ console.error(ui.error('No value provided.'));
63
+ process.exit(1);
64
+ }
65
+
66
+ // Parse numeric values for dimensions
67
+ const storedValue = key === 'default-dimensions' ? parseInt(value, 10) : value;
68
+ setConfigValue(internalKey, storedValue);
69
+ console.log(ui.success(`Set ${ui.cyan(key)} = ${SECRET_KEYS.has(internalKey) ? ui.dim(maskSecret(String(storedValue))) : storedValue}`));
70
+ });
71
+
72
+ // ── config get [key] ──
73
+ configCmd
74
+ .command('get [key]')
75
+ .description('Get a config value (or all if no key)')
76
+ .action((key) => {
77
+ if (key) {
78
+ const internalKey = KEY_MAP[key];
79
+ if (!internalKey) {
80
+ console.error(ui.error(`Unknown config key "${ui.cyan(key)}".`));
81
+ console.error(`Valid keys: ${VALID_KEYS.join(', ')}`);
82
+ process.exit(1);
83
+ }
84
+
85
+ const value = getConfigValue(internalKey);
86
+ if (value === undefined) {
87
+ console.log(`${ui.cyan(key)}: ${ui.dim('(not set)')}`);
88
+ } else {
89
+ const display = SECRET_KEYS.has(internalKey) ? ui.dim(maskSecret(String(value))) : value;
90
+ console.log(`${ui.cyan(key)}: ${display}`);
91
+ }
92
+ } else {
93
+ // Show all config
94
+ const config = loadConfig();
95
+ if (Object.keys(config).length === 0) {
96
+ console.log(ui.dim('No configuration set.'));
97
+ console.log(`Run: ${ui.cyan('vai config set <key> <value>')}`);
98
+ return;
99
+ }
100
+
101
+ // Build a reverse map: internalKey → cliKey
102
+ const reverseMap = {};
103
+ for (const [cliKey, intKey] of Object.entries(KEY_MAP)) {
104
+ reverseMap[intKey] = cliKey;
105
+ }
106
+
107
+ for (const [intKey, value] of Object.entries(config)) {
108
+ const cliKey = reverseMap[intKey] || intKey;
109
+ const display = SECRET_KEYS.has(intKey) ? ui.dim(maskSecret(String(value))) : value;
110
+ console.log(`${ui.cyan(cliKey)}: ${display}`);
111
+ }
112
+ }
113
+ });
114
+
115
+ // ── config delete <key> ──
116
+ configCmd
117
+ .command('delete <key>')
118
+ .description('Remove a config value')
119
+ .action((key) => {
120
+ const internalKey = KEY_MAP[key];
121
+ if (!internalKey) {
122
+ console.error(ui.error(`Unknown config key "${ui.cyan(key)}".`));
123
+ console.error(`Valid keys: ${VALID_KEYS.join(', ')}`);
124
+ process.exit(1);
125
+ }
126
+
127
+ deleteConfigValue(internalKey);
128
+ console.log(ui.success(`Deleted ${ui.cyan(key)}`));
129
+ });
130
+
131
+ // ── config path ──
132
+ configCmd
133
+ .command('path')
134
+ .description('Print the config file path')
135
+ .action(() => {
136
+ console.log(CONFIG_PATH);
137
+ });
138
+
139
+ // ── config reset ──
140
+ configCmd
141
+ .command('reset')
142
+ .description('Delete the entire config file')
143
+ .action(() => {
144
+ try {
145
+ fs.unlinkSync(CONFIG_PATH);
146
+ console.log(ui.success(`Config file deleted: ${ui.dim(CONFIG_PATH)}`));
147
+ } catch (err) {
148
+ if (err.code === 'ENOENT') {
149
+ console.log(ui.dim('No config file found. Nothing to reset.'));
150
+ } else {
151
+ throw err;
152
+ }
153
+ }
154
+ });
155
+ }
156
+
157
+ module.exports = { registerConfig };