voyageai-cli 1.26.1 → 1.27.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voyageai-cli",
3
- "version": "1.26.1",
3
+ "version": "1.27.0",
4
4
  "description": "CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search",
5
5
  "bin": {
6
6
  "vai": "./src/cli.js"
@@ -4,7 +4,7 @@ const os = require('os');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const pc = require('picocolors');
7
- const { getConfigValue } = require('../lib/config');
7
+ const { getConfigValue, setConfigValue, CONFIG_DIR, CONFIG_PATH } = require('../lib/config');
8
8
  const { getApiBase } = require('../lib/api');
9
9
 
10
10
  /**
@@ -240,15 +240,83 @@ async function checkConfig() {
240
240
  };
241
241
  }
242
242
 
243
+ // ── Fix functions (for --fix mode) ──
244
+
245
+ async function fixApiKey() {
246
+ const readline = require('readline');
247
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
248
+
249
+ return new Promise((resolve) => {
250
+ console.log(pc.cyan('\n Fixing: Voyage AI API Key'));
251
+ console.log(pc.dim(' Get a key at: https://dash.voyageai.com/api-keys\n'));
252
+
253
+ rl.question(' Enter your Voyage AI API key: ', (key) => {
254
+ rl.close();
255
+ const trimmed = (key || '').trim();
256
+ if (!trimmed) {
257
+ console.log(pc.yellow(' Skipped (no key entered)'));
258
+ resolve(false);
259
+ return;
260
+ }
261
+ try {
262
+ setConfigValue('apiKey', trimmed);
263
+ // Also set in env for the current session so the connection check passes
264
+ process.env.VOYAGE_API_KEY = trimmed;
265
+ console.log(pc.green(' ✓ API key saved to ~/.vai/config.json'));
266
+ resolve(true);
267
+ } catch (err) {
268
+ console.log(pc.red(` ✗ Failed to save: ${err.message}`));
269
+ resolve(false);
270
+ }
271
+ });
272
+ });
273
+ }
274
+
275
+ function fixConfigPermissions() {
276
+ try {
277
+ fs.chmodSync(CONFIG_PATH, 0o600);
278
+ console.log(pc.green(' ✓ Fixed ~/.vai/config.json permissions to 600'));
279
+ return true;
280
+ } catch (err) {
281
+ console.log(pc.red(` ✗ Failed to fix permissions: ${err.message}`));
282
+ return false;
283
+ }
284
+ }
285
+
286
+ function fixConfigDir() {
287
+ try {
288
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
289
+ console.log(pc.green(' ✓ Created ~/.vai/ directory'));
290
+ return true;
291
+ } catch (err) {
292
+ console.log(pc.red(` ✗ Failed to create directory: ${err.message}`));
293
+ return false;
294
+ }
295
+ }
296
+
297
+ async function fixPdfParse() {
298
+ const { execSync } = require('child_process');
299
+ console.log(pc.cyan('\n Installing pdf-parse...'));
300
+ try {
301
+ execSync('npm install pdf-parse', { stdio: 'pipe' });
302
+ console.log(pc.green(' ✓ pdf-parse installed'));
303
+ return true;
304
+ } catch (err) {
305
+ console.log(pc.red(` ✗ Failed to install: ${err.message}`));
306
+ return false;
307
+ }
308
+ }
309
+
243
310
  async function runDoctor(options = {}) {
244
- const { json, verbose } = options;
245
-
311
+ const { json, verbose, fix } = options;
312
+
246
313
  console.log(pc.bold('\n🩺 Voyage AI CLI Health Check\n'));
247
-
314
+
248
315
  const results = {};
249
316
  let hasError = false;
250
317
  let hasWarning = false;
251
-
318
+ const fixable = [];
319
+
252
320
  // Run all checks
253
321
  const checks = [
254
322
  { key: 'node', fn: checkNodeVersion },
@@ -258,12 +326,12 @@ async function runDoctor(options = {}) {
258
326
  { key: 'pdfParse', fn: checkPdfParse },
259
327
  { key: 'config', fn: checkConfig },
260
328
  ];
261
-
329
+
262
330
  for (const { key, fn } of checks) {
263
331
  const check = CHECKS[key];
264
332
  const result = await fn();
265
333
  results[key] = result;
266
-
334
+
267
335
  // Determine status icon
268
336
  let icon;
269
337
  if (result.ok === true) {
@@ -276,15 +344,23 @@ async function runDoctor(options = {}) {
276
344
  icon = warnMark();
277
345
  hasWarning = true;
278
346
  }
279
-
347
+
280
348
  // Print result
281
349
  console.log(` ${icon} ${pc.bold(check.name)}: ${result.message}`);
282
-
350
+
283
351
  if (result.hint && (verbose || result.ok === false)) {
284
352
  console.log(` ${pc.dim(result.hint)}`);
285
353
  }
354
+
355
+ // Track fixable issues
356
+ if (result.ok !== true) {
357
+ if (key === 'apiKey' && result.ok === false) fixable.push('apiKey');
358
+ if (key === 'pdfParse' && result.ok === null) fixable.push('pdfParse');
359
+ if (key === 'config' && result.message && result.message.includes('permissions')) fixable.push('configPerms');
360
+ if (key === 'config' && result.ok === null) fixable.push('configDir');
361
+ }
286
362
  }
287
-
363
+
288
364
  // Summary
289
365
  console.log('');
290
366
  if (hasError) {
@@ -294,7 +370,43 @@ async function runDoctor(options = {}) {
294
370
  } else {
295
371
  console.log(pc.green(' ✓ All checks passed. vai is ready to use!\n'));
296
372
  }
297
-
373
+
374
+ // --fix mode: attempt automatic repairs
375
+ if (fix && fixable.length > 0) {
376
+ console.log(pc.bold(' 🔧 Attempting fixes...\n'));
377
+ let fixed = 0;
378
+
379
+ for (const item of fixable) {
380
+ if (item === 'configDir') {
381
+ if (fixConfigDir()) fixed++;
382
+ }
383
+ if (item === 'apiKey') {
384
+ if (await fixApiKey()) fixed++;
385
+ }
386
+ if (item === 'configPerms') {
387
+ if (fixConfigPermissions()) fixed++;
388
+ }
389
+ if (item === 'pdfParse') {
390
+ if (await fixPdfParse()) fixed++;
391
+ }
392
+ }
393
+
394
+ console.log('');
395
+ if (fixed > 0) {
396
+ console.log(pc.green(` ✓ Fixed ${fixed} issue${fixed === 1 ? '' : 's'}. Run ${pc.bold('vai doctor')} again to verify.\n`));
397
+ } else {
398
+ console.log(pc.yellow(' No issues were fixed. See hints above for manual steps.\n'));
399
+ }
400
+ return 0;
401
+ } else if (fix && fixable.length === 0 && (hasError || hasWarning)) {
402
+ console.log(pc.dim(' No auto-fixable issues found. See hints above for manual steps.\n'));
403
+ }
404
+
405
+ // Suggest --fix if there are fixable issues and not in fix mode
406
+ if (!fix && fixable.length > 0) {
407
+ console.log(pc.dim(` Tip: run ${pc.bold('vai doctor --fix')} to attempt automatic repairs\n`));
408
+ }
409
+
298
410
  // Suggest next steps
299
411
  if (!hasError) {
300
412
  console.log(pc.dim(' Next steps:'));
@@ -302,11 +414,11 @@ async function runDoctor(options = {}) {
302
414
  console.log(pc.dim(' vai quickstart — Zero-to-search tutorial'));
303
415
  console.log(pc.dim(' vai explain — Learn key concepts\n'));
304
416
  }
305
-
417
+
306
418
  if (json) {
307
419
  console.log(JSON.stringify(results, null, 2));
308
420
  }
309
-
421
+
310
422
  return hasError ? 1 : 0;
311
423
  }
312
424
 
@@ -316,10 +428,41 @@ function register(program) {
316
428
  .description('Run health checks on your vai setup')
317
429
  .option('--json', 'Output results as JSON')
318
430
  .option('-v, --verbose', 'Show hints for all checks')
431
+ .option('-f, --fix', 'Attempt to fix issues automatically')
319
432
  .action(async (options) => {
320
433
  const exitCode = await runDoctor(options);
321
434
  if (exitCode !== 0) process.exit(exitCode);
322
435
  });
323
436
  }
324
437
 
325
- module.exports = { register, runDoctor };
438
+ /**
439
+ * Run all checks programmatically and return structured results.
440
+ * Used by the playground /api/doctor endpoint.
441
+ */
442
+ async function runChecks() {
443
+ const checks = [
444
+ { key: 'node', fn: checkNodeVersion },
445
+ { key: 'apiKey', fn: checkApiKey },
446
+ { key: 'apiConnection', fn: checkApiConnection },
447
+ { key: 'mongodb', fn: checkMongoDB },
448
+ { key: 'pdfParse', fn: checkPdfParse },
449
+ { key: 'config', fn: checkConfig },
450
+ ];
451
+
452
+ const results = {};
453
+ for (const { key, fn } of checks) {
454
+ const result = await fn();
455
+ // Strip picocolors ANSI codes from messages for JSON output
456
+ const clean = (s) => typeof s === 'string' ? s.replace(/\x1b\[[0-9;]*m/g, '') : s;
457
+ results[key] = {
458
+ name: CHECKS[key].name,
459
+ required: CHECKS[key].required,
460
+ ok: result.ok,
461
+ message: clean(result.message),
462
+ hint: result.hint ? clean(result.hint) : null,
463
+ };
464
+ }
465
+ return results;
466
+ }
467
+
468
+ module.exports = { register, runDoctor, runChecks };
@@ -309,6 +309,20 @@ function createPlaygroundServer() {
309
309
  return;
310
310
  }
311
311
 
312
+ // API: Doctor health checks
313
+ if (req.method === 'GET' && req.url === '/api/doctor') {
314
+ try {
315
+ const { runChecks } = require('./doctor');
316
+ const results = await runChecks();
317
+ res.writeHead(200, { 'Content-Type': 'application/json' });
318
+ res.end(JSON.stringify(results));
319
+ } catch (err) {
320
+ res.writeHead(500, { 'Content-Type': 'application/json' });
321
+ res.end(JSON.stringify({ error: err.message }));
322
+ }
323
+ return;
324
+ }
325
+
312
326
  // API: Settings origins — where each config value comes from
313
327
  if (req.method === 'GET' && req.url === '/api/settings/origins') {
314
328
  const { resolveLLMConfig } = require('../lib/llm');
@@ -338,6 +352,40 @@ function createPlaygroundServer() {
338
352
  return;
339
353
  }
340
354
 
355
+ // API: List built-in workflows
356
+ if (req.method === 'GET' && req.url === '/api/workflows') {
357
+ try {
358
+ const { listBuiltinWorkflows } = require('../lib/workflow');
359
+ const workflows = listBuiltinWorkflows();
360
+ res.writeHead(200, { 'Content-Type': 'application/json' });
361
+ res.end(JSON.stringify({ workflows }));
362
+ } catch (err) {
363
+ res.writeHead(500, { 'Content-Type': 'application/json' });
364
+ res.end(JSON.stringify({ error: err.message }));
365
+ }
366
+ return;
367
+ }
368
+
369
+ // API: Get a specific workflow by name
370
+ if (req.method === 'GET' && req.url?.startsWith('/api/workflows/')) {
371
+ const name = decodeURIComponent(req.url.replace('/api/workflows/', ''));
372
+ if (!name) {
373
+ res.writeHead(400, { 'Content-Type': 'application/json' });
374
+ res.end(JSON.stringify({ error: 'Workflow name is required' }));
375
+ return;
376
+ }
377
+ try {
378
+ const { loadWorkflow } = require('../lib/workflow');
379
+ const definition = loadWorkflow(name);
380
+ res.writeHead(200, { 'Content-Type': 'application/json' });
381
+ res.end(JSON.stringify({ definition }));
382
+ } catch (err) {
383
+ res.writeHead(404, { 'Content-Type': 'application/json' });
384
+ res.end(JSON.stringify({ error: err.message }));
385
+ }
386
+ return;
387
+ }
388
+
341
389
  // API: Save chat config (POST) — persists to .vai.json
342
390
  // Placed before generic POST handler so it doesn't require Voyage API key
343
391
  if (req.method === 'POST' && req.url === '/api/chat/config') {
@@ -459,8 +507,37 @@ function createPlaygroundServer() {
459
507
  opts: { systemPrompt, db: db || undefined, collection: collection || undefined },
460
508
  })) {
461
509
  if (event.type === 'tool_call') {
462
- const { name, args, timeMs, error } = event.data;
463
- res.write(`event: tool_call\ndata: ${JSON.stringify({ name, args, timeMs, error })}\n\n`);
510
+ const { name, args, timeMs, error, result } = event.data;
511
+ // Build a short human-readable summary of the tool result
512
+ let resultSummary = '';
513
+ if (!error && result) {
514
+ if (name === 'vai_query' || name === 'vai_search') {
515
+ const docs = result.results || result.documents || [];
516
+ resultSummary = `Found ${docs.length} result${docs.length !== 1 ? 's' : ''}`;
517
+ } else if (name === 'vai_collections') {
518
+ const colls = result.collections || [];
519
+ resultSummary = colls.length > 0
520
+ ? colls.map(c => `<code>${c.name || c}</code>`).slice(0, 5).join(', ')
521
+ : 'No collections found';
522
+ } else if (name === 'vai_models') {
523
+ const models = result.models || [];
524
+ resultSummary = `${models.length} model${models.length !== 1 ? 's' : ''} available`;
525
+ } else if (name === 'vai_embed') {
526
+ const dims = result.dimensions || result.embedding?.length || '?';
527
+ resultSummary = `${dims}-dim vector`;
528
+ } else if (name === 'vai_similarity') {
529
+ const score = result.similarity ?? result.score;
530
+ resultSummary = score !== undefined ? `Score: ${Number(score).toFixed(4)}` : '';
531
+ } else if (name === 'vai_rerank') {
532
+ const items = result.results || [];
533
+ resultSummary = `Reranked ${items.length} result${items.length !== 1 ? 's' : ''}`;
534
+ } else if (name === 'vai_estimate') {
535
+ resultSummary = result.recommendation || '';
536
+ } else if (name === 'vai_explain' || name === 'vai_topics') {
537
+ resultSummary = result.title || result.topic || '';
538
+ }
539
+ }
540
+ res.write(`event: tool_call\ndata: ${JSON.stringify({ name, args, timeMs, error, resultSummary })}\n\n`);
464
541
  } else if (event.type === 'chunk') {
465
542
  res.write(`event: chunk\ndata: ${JSON.stringify({ text: event.data })}\n\n`);
466
543
  } else if (event.type === 'done') {
@@ -645,6 +722,120 @@ function createPlaygroundServer() {
645
722
  }));
646
723
  return;
647
724
  }
725
+
726
+ // API: Validate a workflow definition
727
+ if (req.url === '/api/workflows/validate') {
728
+ const { validateWorkflow } = require('../lib/workflow');
729
+ const { definition } = parsed;
730
+ if (!definition) {
731
+ res.writeHead(400, { 'Content-Type': 'application/json' });
732
+ res.end(JSON.stringify({ error: 'definition is required' }));
733
+ return;
734
+ }
735
+ const errors = validateWorkflow(definition);
736
+ res.writeHead(200, { 'Content-Type': 'application/json' });
737
+ res.end(JSON.stringify({ valid: errors.length === 0, errors }));
738
+ return;
739
+ }
740
+
741
+ // API: Get execution plan (layers + dependency graph)
742
+ if (req.url === '/api/workflows/plan') {
743
+ const { buildExecutionPlan, buildDependencyGraph, validateWorkflow } = require('../lib/workflow');
744
+ const { definition } = parsed;
745
+ if (!definition || !definition.steps) {
746
+ res.writeHead(400, { 'Content-Type': 'application/json' });
747
+ res.end(JSON.stringify({ error: 'definition with steps is required' }));
748
+ return;
749
+ }
750
+ const errors = validateWorkflow(definition);
751
+ if (errors.length > 0) {
752
+ res.writeHead(400, { 'Content-Type': 'application/json' });
753
+ res.end(JSON.stringify({ error: 'Invalid workflow', errors }));
754
+ return;
755
+ }
756
+ const layers = buildExecutionPlan(definition.steps);
757
+ const graphMap = buildDependencyGraph(definition.steps);
758
+ // Convert Map<string, Set<string>> to plain object for JSON serialization
759
+ const graph = {};
760
+ for (const [stepId, deps] of graphMap) {
761
+ graph[stepId] = Array.from(deps);
762
+ }
763
+ res.writeHead(200, { 'Content-Type': 'application/json' });
764
+ res.end(JSON.stringify({ layers, graph }));
765
+ return;
766
+ }
767
+
768
+ // API: Execute a workflow (streaming SSE)
769
+ if (req.url === '/api/workflows/execute') {
770
+ const { executeWorkflow, validateWorkflow: validateWf } = require('../lib/workflow');
771
+ const { definition, inputs } = parsed;
772
+ if (!definition) {
773
+ res.writeHead(400, { 'Content-Type': 'application/json' });
774
+ res.end(JSON.stringify({ error: 'definition is required' }));
775
+ return;
776
+ }
777
+ const errors = validateWf(definition);
778
+ if (errors.length > 0) {
779
+ res.writeHead(400, { 'Content-Type': 'application/json' });
780
+ res.end(JSON.stringify({ error: 'Invalid workflow', errors }));
781
+ return;
782
+ }
783
+
784
+ // Stream execution events as SSE
785
+ res.writeHead(200, {
786
+ 'Content-Type': 'text/event-stream',
787
+ 'Cache-Control': 'no-cache',
788
+ 'Connection': 'keep-alive',
789
+ });
790
+
791
+ try {
792
+ const result = await executeWorkflow(definition, {
793
+ inputs: inputs || {},
794
+ onStepStart: (stepId, stepDef) => {
795
+ res.write(`event: step_start\ndata: ${JSON.stringify({
796
+ stepId,
797
+ name: stepDef.name || stepId,
798
+ tool: stepDef.tool,
799
+ })}\n\n`);
800
+ },
801
+ onStepComplete: (stepId, output, timeMs) => {
802
+ // Summarize output to avoid huge payloads
803
+ let summary = '';
804
+ if (output && typeof output === 'object') {
805
+ if (output.results) summary = `${output.results.length} results`;
806
+ else if (output.similarity !== undefined) summary = `similarity: ${Number(output.similarity).toFixed(4)}`;
807
+ else if (output.text) summary = output.text.slice(0, 100) + (output.text.length > 100 ? '...' : '');
808
+ else summary = JSON.stringify(output).slice(0, 200);
809
+ }
810
+ res.write(`event: step_complete\ndata: ${JSON.stringify({
811
+ stepId, timeMs, summary,
812
+ output: JSON.stringify(output).length < 5000 ? output : { _truncated: true, summary },
813
+ })}\n\n`);
814
+ },
815
+ onStepSkip: (stepId, reason) => {
816
+ res.write(`event: step_skip\ndata: ${JSON.stringify({ stepId, reason })}\n\n`);
817
+ },
818
+ onStepError: (stepId, error) => {
819
+ res.write(`event: step_error\ndata: ${JSON.stringify({
820
+ stepId,
821
+ error: error.message || String(error),
822
+ })}\n\n`);
823
+ },
824
+ });
825
+
826
+ res.write(`event: done\ndata: ${JSON.stringify({
827
+ output: result.output,
828
+ totalTimeMs: result.totalTimeMs,
829
+ layers: result.layers,
830
+ steps: result.steps,
831
+ })}\n\n`);
832
+ } catch (err) {
833
+ res.write(`event: error\ndata: ${JSON.stringify({ error: err.message })}\n\n`);
834
+ }
835
+
836
+ res.end();
837
+ return;
838
+ }
648
839
  }
649
840
 
650
841
  // 404
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file