voyageai-cli 1.20.5 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +142 -26
  2. package/README.md +153 -2
  3. package/package.json +1 -1
  4. package/src/cli.js +12 -0
  5. package/src/commands/about.js +39 -3
  6. package/src/commands/doctor.js +325 -0
  7. package/src/commands/eval.js +420 -10
  8. package/src/commands/generate.js +220 -0
  9. package/src/commands/playground.js +93 -0
  10. package/src/commands/purge.js +271 -0
  11. package/src/commands/quickstart.js +203 -0
  12. package/src/commands/refresh.js +322 -0
  13. package/src/commands/scaffold.js +217 -0
  14. package/src/lib/codegen.js +313 -0
  15. package/src/lib/explanations.js +163 -1
  16. package/src/lib/scaffold-structure.js +114 -0
  17. package/src/lib/templates/nextjs/README.md.tpl +106 -0
  18. package/src/lib/templates/nextjs/env.example.tpl +8 -0
  19. package/src/lib/templates/nextjs/layout.jsx.tpl +29 -0
  20. package/src/lib/templates/nextjs/lib-mongo.js.tpl +111 -0
  21. package/src/lib/templates/nextjs/lib-voyage.js.tpl +103 -0
  22. package/src/lib/templates/nextjs/package.json.tpl +33 -0
  23. package/src/lib/templates/nextjs/page-search.jsx.tpl +147 -0
  24. package/src/lib/templates/nextjs/route-ingest.js.tpl +114 -0
  25. package/src/lib/templates/nextjs/route-search.js.tpl +97 -0
  26. package/src/lib/templates/nextjs/theme.js.tpl +84 -0
  27. package/src/lib/templates/python/README.md.tpl +145 -0
  28. package/src/lib/templates/python/app.py.tpl +221 -0
  29. package/src/lib/templates/python/chunker.py.tpl +127 -0
  30. package/src/lib/templates/python/env.example.tpl +12 -0
  31. package/src/lib/templates/python/mongo_client.py.tpl +125 -0
  32. package/src/lib/templates/python/requirements.txt.tpl +10 -0
  33. package/src/lib/templates/python/voyage_client.py.tpl +124 -0
  34. package/src/lib/templates/vanilla/README.md.tpl +156 -0
  35. package/src/lib/templates/vanilla/client.js.tpl +103 -0
  36. package/src/lib/templates/vanilla/connection.js.tpl +126 -0
  37. package/src/lib/templates/vanilla/env.example.tpl +11 -0
  38. package/src/lib/templates/vanilla/ingest.js.tpl +231 -0
  39. package/src/lib/templates/vanilla/package.json.tpl +31 -0
  40. package/src/lib/templates/vanilla/retrieval.js.tpl +100 -0
  41. package/src/lib/templates/vanilla/search-api.js.tpl +175 -0
  42. package/src/lib/templates/vanilla/server.js.tpl +81 -0
  43. package/src/lib/zip.js +130 -0
  44. package/src/playground/index.html +772 -33
@@ -0,0 +1,325 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const pc = require('picocolors');
7
+ const { getConfigValue } = require('../lib/config');
8
+ const { getApiBase } = require('../lib/api');
9
+
10
+ /**
11
+ * vai doctor — Health check command
12
+ * Validates the entire setup chain: Node version, API key, MongoDB connectivity,
13
+ * peer dependencies, and configuration.
14
+ */
15
+
16
+ const CHECKS = {
17
+ node: { name: 'Node.js Version', required: true },
18
+ apiKey: { name: 'Voyage AI API Key', required: true },
19
+ apiConnection: { name: 'Voyage AI API Connection', required: true },
20
+ mongodb: { name: 'MongoDB Connection', required: false },
21
+ pdfParse: { name: 'PDF Support (pdf-parse)', required: false },
22
+ config: { name: 'Configuration Files', required: false },
23
+ };
24
+
25
+ function checkMark(ok) {
26
+ return ok ? pc.green('✓') : pc.red('✗');
27
+ }
28
+
29
+ function warnMark() {
30
+ return pc.yellow('⚠');
31
+ }
32
+
33
+ async function checkNodeVersion() {
34
+ const version = process.version;
35
+ const major = parseInt(version.slice(1).split('.')[0], 10);
36
+ const minVersion = 18;
37
+ const ok = major >= minVersion;
38
+
39
+ return {
40
+ ok,
41
+ message: ok
42
+ ? `${version} (meets minimum v${minVersion})`
43
+ : `${version} — ${pc.red(`requires v${minVersion}+`)}`,
44
+ hint: ok ? null : 'Upgrade Node.js: https://nodejs.org/',
45
+ };
46
+ }
47
+
48
+ async function checkApiKey() {
49
+ const key = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
50
+ const masked = key ? `${key.slice(0, 8)}...${key.slice(-4)}` : null;
51
+
52
+ if (!key) {
53
+ return {
54
+ ok: false,
55
+ message: 'Not configured',
56
+ hint: 'Set via: vai config set api-key YOUR_KEY\n or: export VOYAGE_API_KEY=YOUR_KEY\n Get a key: https://dash.voyageai.com/api-keys',
57
+ };
58
+ }
59
+
60
+ // Check key format (Voyage keys typically start with pa- or similar)
61
+ const validFormat = key.length > 20;
62
+
63
+ return {
64
+ ok: true,
65
+ message: masked,
66
+ hint: validFormat ? null : 'Key format looks unusual — verify at dash.voyageai.com',
67
+ };
68
+ }
69
+
70
+ async function checkApiConnection() {
71
+ const key = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
72
+ if (!key) {
73
+ return {
74
+ ok: false,
75
+ message: 'Skipped (no API key)',
76
+ hint: null,
77
+ };
78
+ }
79
+
80
+ const baseUrl = getApiBase();
81
+
82
+ try {
83
+ const https = require('https');
84
+ const url = require('url');
85
+
86
+ const parsed = new URL(`${baseUrl}/embeddings`);
87
+
88
+ const result = await new Promise((resolve, reject) => {
89
+ const req = https.request({
90
+ hostname: parsed.hostname,
91
+ port: parsed.port || 443,
92
+ path: parsed.pathname,
93
+ method: 'POST',
94
+ headers: {
95
+ 'Authorization': `Bearer ${key}`,
96
+ 'Content-Type': 'application/json',
97
+ },
98
+ timeout: 10000,
99
+ }, (res) => {
100
+ let data = '';
101
+ res.on('data', chunk => data += chunk);
102
+ res.on('end', () => {
103
+ // 200 = success, 400 = bad request (but server reachable), 401 = bad key
104
+ if (res.statusCode === 401) {
105
+ resolve({ ok: false, status: 401, message: 'Invalid API key' });
106
+ } else if (res.statusCode === 400) {
107
+ // Bad request means server is reachable, key is valid
108
+ resolve({ ok: true, status: 400, message: 'Connected' });
109
+ } else if (res.statusCode === 200) {
110
+ resolve({ ok: true, status: 200, message: 'Connected' });
111
+ } else {
112
+ resolve({ ok: false, status: res.statusCode, message: `HTTP ${res.statusCode}` });
113
+ }
114
+ });
115
+ });
116
+
117
+ req.on('error', (err) => reject(err));
118
+ req.on('timeout', () => {
119
+ req.destroy();
120
+ reject(new Error('Connection timeout'));
121
+ });
122
+
123
+ // Send minimal request body
124
+ req.write(JSON.stringify({ model: 'voyage-3-lite', input: ['test'] }));
125
+ req.end();
126
+ });
127
+
128
+ return {
129
+ ok: result.ok,
130
+ message: result.ok ? `${result.message} (${baseUrl})` : result.message,
131
+ hint: result.ok ? null :
132
+ result.status === 401 ? 'API key is invalid — get a new one at dash.voyageai.com' :
133
+ 'Check your network connection and API base URL',
134
+ };
135
+ } catch (err) {
136
+ return {
137
+ ok: false,
138
+ message: `Connection failed: ${err.message}`,
139
+ hint: 'Check your network connection or firewall settings',
140
+ };
141
+ }
142
+ }
143
+
144
+ async function checkMongoDB() {
145
+ const uri = process.env.MONGODB_URI || getConfigValue('mongoUri');
146
+
147
+ if (!uri) {
148
+ return {
149
+ ok: null, // null = not configured (optional)
150
+ message: 'Not configured (optional)',
151
+ hint: 'Set via: vai config set mongo-uri YOUR_URI\n Required for vai store, vai search, vai query',
152
+ };
153
+ }
154
+
155
+ try {
156
+ const { MongoClient } = require('mongodb');
157
+ const client = new MongoClient(uri, {
158
+ serverSelectionTimeoutMS: 5000,
159
+ connectTimeoutMS: 5000,
160
+ });
161
+
162
+ await client.connect();
163
+ await client.db().command({ ping: 1 });
164
+ await client.close();
165
+
166
+ // Mask the URI
167
+ const masked = uri.replace(/:\/\/[^@]+@/, '://***@').replace(/\?.*$/, '');
168
+
169
+ return {
170
+ ok: true,
171
+ message: `Connected (${masked})`,
172
+ hint: null,
173
+ };
174
+ } catch (err) {
175
+ return {
176
+ ok: false,
177
+ message: `Connection failed: ${err.message}`,
178
+ hint: 'Verify your connection string and network access',
179
+ };
180
+ }
181
+ }
182
+
183
+ async function checkPdfParse() {
184
+ try {
185
+ require.resolve('pdf-parse');
186
+ return {
187
+ ok: true,
188
+ message: 'Installed',
189
+ hint: null,
190
+ };
191
+ } catch {
192
+ return {
193
+ ok: null, // null = not installed (optional)
194
+ message: 'Not installed (optional)',
195
+ hint: 'Install for PDF support: npm install pdf-parse',
196
+ };
197
+ }
198
+ }
199
+
200
+ async function checkConfig() {
201
+ const homeConfig = path.join(os.homedir(), '.vai', 'config.json');
202
+ const projectConfig = path.join(process.cwd(), '.vai.json');
203
+
204
+ const results = [];
205
+
206
+ // Check home config
207
+ if (fs.existsSync(homeConfig)) {
208
+ try {
209
+ const stat = fs.statSync(homeConfig);
210
+ const mode = (stat.mode & 0o777).toString(8);
211
+ const secure = mode === '600';
212
+ results.push(`~/.vai/config.json: ${secure ? 'OK' : pc.yellow(`permissions ${mode} (should be 600)`)}`);
213
+ } catch {
214
+ results.push('~/.vai/config.json: exists');
215
+ }
216
+ }
217
+
218
+ // Check project config
219
+ if (fs.existsSync(projectConfig)) {
220
+ try {
221
+ JSON.parse(fs.readFileSync(projectConfig, 'utf8'));
222
+ results.push('.vai.json: OK');
223
+ } catch {
224
+ results.push(`.vai.json: ${pc.red('invalid JSON')}`);
225
+ }
226
+ }
227
+
228
+ if (results.length === 0) {
229
+ return {
230
+ ok: null,
231
+ message: 'No config files found',
232
+ hint: 'Run: vai init (for project config) or vai config set (for user config)',
233
+ };
234
+ }
235
+
236
+ return {
237
+ ok: true,
238
+ message: results.join(', '),
239
+ hint: null,
240
+ };
241
+ }
242
+
243
+ async function runDoctor(options = {}) {
244
+ const { json, verbose } = options;
245
+
246
+ console.log(pc.bold('\n🩺 Voyage AI CLI Health Check\n'));
247
+
248
+ const results = {};
249
+ let hasError = false;
250
+ let hasWarning = false;
251
+
252
+ // Run all checks
253
+ const checks = [
254
+ { key: 'node', fn: checkNodeVersion },
255
+ { key: 'apiKey', fn: checkApiKey },
256
+ { key: 'apiConnection', fn: checkApiConnection },
257
+ { key: 'mongodb', fn: checkMongoDB },
258
+ { key: 'pdfParse', fn: checkPdfParse },
259
+ { key: 'config', fn: checkConfig },
260
+ ];
261
+
262
+ for (const { key, fn } of checks) {
263
+ const check = CHECKS[key];
264
+ const result = await fn();
265
+ results[key] = result;
266
+
267
+ // Determine status icon
268
+ let icon;
269
+ if (result.ok === true) {
270
+ icon = checkMark(true);
271
+ } else if (result.ok === false) {
272
+ icon = checkMark(false);
273
+ if (check.required) hasError = true;
274
+ else hasWarning = true;
275
+ } else {
276
+ icon = warnMark();
277
+ hasWarning = true;
278
+ }
279
+
280
+ // Print result
281
+ console.log(` ${icon} ${pc.bold(check.name)}: ${result.message}`);
282
+
283
+ if (result.hint && (verbose || result.ok === false)) {
284
+ console.log(` ${pc.dim(result.hint)}`);
285
+ }
286
+ }
287
+
288
+ // Summary
289
+ console.log('');
290
+ if (hasError) {
291
+ console.log(pc.red(' ✗ Some required checks failed. Fix the issues above to use vai.\n'));
292
+ } else if (hasWarning) {
293
+ console.log(pc.yellow(' ⚠ Some optional features are not configured.\n'));
294
+ } else {
295
+ console.log(pc.green(' ✓ All checks passed. vai is ready to use!\n'));
296
+ }
297
+
298
+ // Suggest next steps
299
+ if (!hasError) {
300
+ console.log(pc.dim(' Next steps:'));
301
+ console.log(pc.dim(' vai demo — Interactive walkthrough'));
302
+ console.log(pc.dim(' vai quickstart — Zero-to-search tutorial'));
303
+ console.log(pc.dim(' vai explain — Learn key concepts\n'));
304
+ }
305
+
306
+ if (json) {
307
+ console.log(JSON.stringify(results, null, 2));
308
+ }
309
+
310
+ return hasError ? 1 : 0;
311
+ }
312
+
313
+ function register(program) {
314
+ program
315
+ .command('doctor')
316
+ .description('Run health checks on your vai setup')
317
+ .option('--json', 'Output results as JSON')
318
+ .option('-v, --verbose', 'Show hints for all checks')
319
+ .action(async (options) => {
320
+ const exitCode = await runDoctor(options);
321
+ if (exitCode !== 0) process.exit(exitCode);
322
+ });
323
+ }
324
+
325
+ module.exports = { register, runDoctor };