voyageai-cli 1.30.1 → 1.30.2

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 (37) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -1
  3. package/src/cli.js +2 -0
  4. package/src/commands/about.js +3 -3
  5. package/src/commands/code-search.js +751 -0
  6. package/src/commands/doctor.js +1 -1
  7. package/src/commands/index-workspace.js +9 -5
  8. package/src/commands/playground.js +9 -1
  9. package/src/commands/quickstart.js +4 -4
  10. package/src/commands/workflow.js +132 -65
  11. package/src/lib/catalog.js +4 -2
  12. package/src/lib/code-search.js +315 -0
  13. package/src/lib/codegen.js +1 -1
  14. package/src/lib/explanations.js +3 -3
  15. package/src/lib/github.js +226 -0
  16. package/src/lib/template-engine.js +154 -20
  17. package/src/lib/workflow-builder.js +753 -0
  18. package/src/lib/workflow-formatters.js +454 -0
  19. package/src/lib/workflow-input-cache.js +111 -0
  20. package/src/lib/workflow-scaffold.js +1 -1
  21. package/src/lib/workflow.js +91 -1
  22. package/src/mcp/schemas/index.js +130 -0
  23. package/src/mcp/server.js +17 -4
  24. package/src/mcp/tools/authoring.js +662 -0
  25. package/src/mcp/tools/code-search.js +620 -0
  26. package/src/mcp/tools/ingest.js +2 -5
  27. package/src/mcp/tools/retrieval.js +2 -15
  28. package/src/mcp/tools/workspace.js +1 -12
  29. package/src/mcp/utils.js +20 -0
  30. package/src/playground/help/workflow-nodes.js +127 -2
  31. package/src/playground/index.html +1366 -24
  32. package/src/workflows/code-review.json +110 -0
  33. package/src/workflows/cost-analysis.json +5 -0
  34. package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
  35. package/src/workflows/tests/code-review.happy-path.test.json +121 -0
  36. package/src/workflows/tests/code-review.no-question.test.json +70 -0
  37. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +2 -2
@@ -121,7 +121,7 @@ async function checkApiConnection() {
121
121
  });
122
122
 
123
123
  // Send minimal request body
124
- req.write(JSON.stringify({ model: 'voyage-3-lite', input: ['test'] }));
124
+ req.write(JSON.stringify({ model: 'voyage-4-lite', input: ['test'] }));
125
125
  req.end();
126
126
  });
127
127
 
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
- const ora = require('ora');
4
+ let ora;
5
+ async function getOra() {
6
+ if (!ora) { ora = (await import('ora')).default; }
7
+ return ora;
8
+ }
5
9
  const pc = require('picocolors');
6
10
 
7
11
  /**
@@ -31,7 +35,7 @@ function registerIndexWorkspace(program) {
31
35
  const { handleIndexWorkspace } = require('../mcp/tools/workspace');
32
36
  const resolvedPath = workspacePath ? path.resolve(workspacePath) : process.cwd();
33
37
 
34
- const spinner = ora(`Indexing ${resolvedPath}...`).start();
38
+ const spinner = (await getOra())(`Indexing ${resolvedPath}...`).start();
35
39
 
36
40
  try {
37
41
  const result = await handleIndexWorkspace({
@@ -74,7 +78,7 @@ function registerIndexWorkspace(program) {
74
78
 
75
79
  // Create index if requested
76
80
  if (opts.createIndex) {
77
- const indexSpinner = ora('Creating vector search index...').start();
81
+ const indexSpinner = (await getOra())('Creating vector search index...').start();
78
82
  try {
79
83
  const { createVectorIndex } = require('../lib/mongo');
80
84
  await createVectorIndex(
@@ -112,7 +116,7 @@ function registerIndexWorkspace(program) {
112
116
  telemetry.send('cli_search_code_run', { language: opts.language });
113
117
 
114
118
  const { handleSearchCode } = require('../mcp/tools/workspace');
115
- const spinner = ora('Searching...').start();
119
+ const spinner = (await getOra())('Searching...').start();
116
120
 
117
121
  try {
118
122
  const result = await handleSearchCode({
@@ -198,7 +202,7 @@ function registerIndexWorkspace(program) {
198
202
  process.exit(1);
199
203
  }
200
204
 
201
- const spinner = ora('Finding relevant context...').start();
205
+ const spinner = (await getOra())('Finding relevant context...').start();
202
206
 
203
207
  try {
204
208
  const result = await handleExplainCode({
@@ -134,7 +134,7 @@ function registerPlayground(program) {
134
134
  */
135
135
  function createPlaygroundServer() {
136
136
  const { getApiBase, requireApiKey, generateEmbeddings } = require('../lib/api');
137
- const { MODEL_CATALOG } = require('../lib/catalog');
137
+ const { MODEL_CATALOG, BENCHMARK_SCORES } = require('../lib/catalog');
138
138
  const { cosineSimilarity } = require('../lib/math');
139
139
  const { getConfigValue } = require('../lib/config');
140
140
 
@@ -273,6 +273,13 @@ function createPlaygroundServer() {
273
273
  return;
274
274
  }
275
275
 
276
+ // API: Full Model Catalog (for Models tab)
277
+ if (req.method === 'GET' && req.url === '/api/models/catalog') {
278
+ res.writeHead(200, { 'Content-Type': 'application/json' });
279
+ res.end(JSON.stringify({ models: MODEL_CATALOG, benchmarks: BENCHMARK_SCORES }));
280
+ return;
281
+ }
282
+
276
283
  // API: Generate code
277
284
  if (req.method === 'POST' && req.url === '/api/generate') {
278
285
  let body = '';
@@ -1604,6 +1611,7 @@ function createPlaygroundServer() {
1604
1611
  totalTimeMs: result.totalTimeMs,
1605
1612
  layers: result.layers,
1606
1613
  steps: result.steps,
1614
+ formatters: result.formatters || null,
1607
1615
  })}\n\n`);
1608
1616
  } catch (err) {
1609
1617
  res.write(`event: error\ndata: ${JSON.stringify({ error: err.message })}\n\n`);
@@ -83,7 +83,7 @@ We'll embed these ${SAMPLE_DOCS.length} sample documents:
83
83
  console.log(pc.bold('\nStep 2: Embedding Documents'));
84
84
  console.log(pc.dim('─'.repeat(40)));
85
85
  console.log(`
86
- Running: ${pc.cyan('vai embed --model voyage-3-lite')}
86
+ Running: ${pc.cyan('vai embed --model voyage-4-lite')}
87
87
  `);
88
88
 
89
89
  let embeddings;
@@ -91,7 +91,7 @@ Running: ${pc.cyan('vai embed --model voyage-3-lite')}
91
91
  process.stdout.write(' Embedding documents... ');
92
92
  const result = await embed({
93
93
  texts: SAMPLE_DOCS,
94
- model: 'voyage-3-lite',
94
+ model: 'voyage-4-lite',
95
95
  inputType: 'document',
96
96
  });
97
97
  embeddings = result.embeddings;
@@ -99,7 +99,7 @@ Running: ${pc.cyan('vai embed --model voyage-3-lite')}
99
99
  console.log(`
100
100
  ${pc.green('✓')} Created ${embeddings.length} embeddings
101
101
  ${pc.dim(` Dimensions: ${embeddings[0].length}`)}
102
- ${pc.dim(` Model: voyage-3-lite`)}
102
+ ${pc.dim(` Model: voyage-4-lite`)}
103
103
  `);
104
104
  } catch (err) {
105
105
  console.log(pc.red('✗'));
@@ -123,7 +123,7 @@ Query: "${pc.cyan(query)}"
123
123
  process.stdout.write(' Embedding query... ');
124
124
  const queryResult = await embed({
125
125
  texts: [query],
126
- model: 'voyage-3-lite',
126
+ model: 'voyage-4-lite',
127
127
  inputType: 'query',
128
128
  });
129
129
  const queryEmbedding = queryResult.embeddings[0];
@@ -48,8 +48,13 @@ async function promptForInputs(definition, existingInputs) {
48
48
  const { buildInputSteps } = require('../lib/workflow');
49
49
  const { createCLIRenderer } = require('../lib/wizard-cli');
50
50
  const { runWizard } = require('../lib/wizard');
51
+ const { loadInputCache } = require('../lib/workflow-input-cache');
51
52
 
52
- const allSteps = buildInputSteps(definition);
53
+ // Load cached inputs from last run to use as defaults
54
+ const workflowName = definition.name || '';
55
+ const cachedInputs = loadInputCache(workflowName);
56
+
57
+ const allSteps = buildInputSteps(definition, cachedInputs);
53
58
  // Only prompt for inputs not already provided
54
59
  const steps = allSteps.filter(s => !(s.id in existingInputs));
55
60
  if (steps.length === 0) return existingInputs;
@@ -93,6 +98,7 @@ function registerWorkflow(program) {
93
98
  .option('--db <name>', 'Override default database')
94
99
  .option('--collection <name>', 'Override default collection')
95
100
  .option('--json', 'Output results as JSON', false)
101
+ .option('-o, --output <format>', 'Output format: json, table, markdown, text, csv, value:<path>')
96
102
  .option('--quiet', 'Suppress progress output', false)
97
103
  .option('--dry-run', 'Show execution plan without running', false)
98
104
  .option('--verbose', 'Show step details', false)
@@ -232,33 +238,24 @@ function registerWorkflow(program) {
232
238
 
233
239
  wfDone();
234
240
 
241
+ // Cache inputs for next run
242
+ try {
243
+ const { saveInputCache } = require('../lib/workflow-input-cache');
244
+ saveInputCache(workflowName, opts.input);
245
+ } catch { /* non-critical, don't fail the run */ }
246
+
235
247
  // Output
236
- if (opts.json) {
248
+ const { formatWorkflowOutput, autoDetectFormat } = require('../lib/workflow-formatters');
249
+ const fmtHints = definition.formatters || {};
250
+
251
+ if (opts.json || opts.output === 'json') {
237
252
  console.log(JSON.stringify(result.output, null, 2));
253
+ } else if (opts.output) {
254
+ console.log(formatWorkflowOutput(result.output, opts.output, fmtHints));
238
255
  } else if (result.output) {
239
- // Pretty-print top results if they exist
240
- const output = result.output;
241
- if (output.results && Array.isArray(output.results)) {
242
- const top = output.results.slice(0, 5);
243
- console.log(pc.bold('Top results:'));
244
- for (let i = 0; i < top.length; i++) {
245
- const r = top[i];
246
- const source = r.source || r.text?.slice(0, 50) || `result ${i + 1}`;
247
- const score = r.score != null ? ` (${r.score.toFixed(2)})` : '';
248
- console.log(` ${pc.dim(`[${i + 1}]`)} ${source}${pc.dim(score)}`);
249
- }
250
- } else if (output.summary) {
251
- console.log(output.summary);
252
- } else if (output.comparison) {
253
- console.log(pc.bold('Cost comparison:'));
254
- for (const item of output.comparison) {
255
- if (item && item.model) {
256
- console.log(` ${pc.cyan(item.model)}: $${item.totalCost} total (embed: $${item.embeddingCost}, queries: $${item.monthlyQueryCost}/mo)`);
257
- }
258
- }
259
- } else {
260
- console.log(JSON.stringify(output, null, 2));
261
- }
256
+ // Auto-detect best format from output shape and workflow hints
257
+ const bestFormat = autoDetectFormat(result.output, fmtHints);
258
+ console.log(formatWorkflowOutput(result.output, bestFormat, fmtHints));
262
259
  }
263
260
  } catch (err) {
264
261
  console.error(ui.error(err.message));
@@ -862,8 +859,9 @@ function registerWorkflow(program) {
862
859
  // ── workflow create ──
863
860
  wfCmd
864
861
  .command('create')
865
- .description('Scaffold a publish-ready npm package from a workflow')
862
+ .description('Interactive workflow builder -- scaffold a validated, publish-ready workflow package')
866
863
  .option('--from <file>', 'Existing workflow JSON to package')
864
+ .option('--from-description <desc>', 'Generate a workflow skeleton from a text description')
867
865
  .option('--name <name>', 'Package name (without vai-workflow- prefix)')
868
866
  .option('--author <name>', 'Author name')
869
867
  .option('--description <desc>', 'Package description')
@@ -871,8 +869,9 @@ function registerWorkflow(program) {
871
869
  .option('--scope <scope>', 'Package scope (e.g. "vaicli" for @vaicli/vai-workflow-*)')
872
870
  .option('--output <dir>', 'Output directory')
873
871
  .action(async (opts) => {
874
- const { scaffoldPackage, toPackageName, CATEGORIES, emptyWorkflowTemplate } = require('../lib/workflow-scaffold');
875
- const { loadWorkflow } = require('../lib/workflow');
872
+ const { scaffoldPackage, toPackageName, CATEGORIES } = require('../lib/workflow-scaffold');
873
+ const { loadWorkflow, validateWorkflow, buildExecutionPlan } = require('../lib/workflow');
874
+ const { runInteractiveBuilder, workflowFromDescription } = require('../lib/workflow-builder');
876
875
 
877
876
  let definition;
878
877
  let name = opts.name;
@@ -894,50 +893,85 @@ function registerWorkflow(program) {
894
893
  if (!description) {
895
894
  description = definition.description;
896
895
  }
897
- } else if (process.stdin.isTTY) {
898
- // Interactive mode
896
+ } else if (opts.fromDescription) {
897
+ // Generate from text description
899
898
  try {
899
+ definition = workflowFromDescription(opts.fromDescription);
900
+ name = name || definition.name;
901
+ description = description || definition.description;
902
+
900
903
  const p = require('@clack/prompts');
901
- p.intro(pc.bold('Create a new workflow package'));
902
-
903
- const answers = await p.group({
904
- name: () => p.text({ message: 'Workflow name', placeholder: 'my-workflow', validate: v => v ? undefined : 'Required' }),
905
- description: () => p.text({ message: 'Description', placeholder: 'A brief description of what this workflow does' }),
906
- category: () => p.select({
907
- message: 'Category',
908
- options: CATEGORIES.map(c => ({ value: c, label: c })),
909
- }),
910
- author: () => p.text({ message: 'Author', placeholder: 'Your Name', defaultValue: getGitAuthor() }),
911
- });
912
-
913
- if (p.isCancel(answers)) {
914
- p.cancel('Cancelled.');
915
- process.exit(0);
904
+ p.intro(pc.bold('Generated workflow from description'));
905
+
906
+ // Show what was generated
907
+ p.log.info(`Name: ${pc.cyan(definition.name)}`);
908
+ p.log.info(`Steps: ${definition.steps.map(s => `${pc.cyan(s.id)} (${s.tool})`).join(' -> ')}`);
909
+ p.log.info(`Inputs: ${Object.keys(definition.inputs).join(', ') || 'none'}`);
910
+
911
+ // Show execution plan
912
+ try {
913
+ const layers = buildExecutionPlan(definition.steps);
914
+ p.log.info(pc.bold('Execution plan:'));
915
+ for (let i = 0; i < layers.length; i++) {
916
+ p.log.message(` Layer ${i + 1}: ${layers[i].join(', ')}`);
917
+ }
918
+ } catch (e) { /* skip */ }
919
+
920
+ // Validate
921
+ const errors = validateWorkflow(definition);
922
+ if (errors.length > 0) {
923
+ p.log.warn('Validation issues:');
924
+ for (const err of errors) {
925
+ p.log.warn(` ${err}`);
926
+ }
927
+ } else {
928
+ p.log.success('Workflow validates successfully!');
916
929
  }
917
930
 
918
- name = answers.name;
919
- description = answers.description;
920
- category = answers.category;
921
- author = answers.author;
922
- definition = emptyWorkflowTemplate();
923
- definition.name = name;
924
- definition.description = description || '';
925
- // Add a placeholder step so validation passes
926
- definition.steps = [{
927
- id: 'search',
928
- tool: 'query',
929
- name: 'Search',
930
- inputs: { query: '{{ inputs.query }}' },
931
- }];
932
- definition.inputs = {
933
- query: { type: 'string', required: true, description: 'Search query' },
934
- };
931
+ // Ask for category if not provided
932
+ if (!category) {
933
+ const { guessCategory, extractTools } = require('../lib/workflow-scaffold');
934
+ category = guessCategory(extractTools(definition));
935
+ }
936
+
937
+ // Ask for author if not provided
938
+ if (!author) {
939
+ author = getGitAuthor();
940
+ }
941
+
942
+ // Confirm or edit
943
+ const proceed = await p.confirm({ message: 'Scaffold this workflow as a package?', initialValue: true });
944
+ if (p.isCancel(proceed) || !proceed) {
945
+ // Write just the workflow.json for manual editing
946
+ const fs = require('fs');
947
+ const filename = `${definition.name}.vai-workflow.json`;
948
+ fs.writeFileSync(filename, JSON.stringify(definition, null, 2) + '\n');
949
+ p.log.info(`Wrote ${pc.cyan(filename)} for manual editing.`);
950
+ p.outro('Edit the file and run `vai workflow create --from <file>` when ready.');
951
+ return;
952
+ }
935
953
  } catch (err) {
954
+ console.error(ui.error(`Generation failed: ${err.message}`));
955
+ process.exit(1);
956
+ }
957
+ } else if (process.stdin.isTTY) {
958
+ // Full interactive builder
959
+ try {
960
+ const result = await runInteractiveBuilder();
961
+ definition = result.definition;
962
+ name = name || result.name;
963
+ description = description || result.description;
964
+ category = category || result.category;
965
+ author = author || result.author;
966
+ } catch (err) {
967
+ if (err.message && err.message.includes('cancelled')) {
968
+ process.exit(0);
969
+ }
936
970
  console.error(ui.error(`Interactive mode failed: ${err.message}`));
937
971
  process.exit(1);
938
972
  }
939
973
  } else {
940
- console.error(ui.error('Provide --from <file> or run interactively (TTY required).'));
974
+ console.error(ui.error('Provide --from <file>, --from-description "text", or run interactively (TTY required).'));
941
975
  process.exit(1);
942
976
  }
943
977
 
@@ -965,8 +999,8 @@ function registerWorkflow(program) {
965
999
  }
966
1000
  console.log();
967
1001
  console.log('Next steps:');
968
- console.log(` 1. ${opts.from ? '' : pc.dim('Edit workflow.json with your workflow definition')}${opts.from ? 'Review README.md' : ''}`);
969
- console.log(` 2. cd ${pkgName}`);
1002
+ console.log(` 1. ${opts.from ? 'Review README.md' : `cd ${pkgName} && review workflow.json`}`);
1003
+ console.log(` 2. ${pc.dim('vai workflow validate')} ${pkgName}/workflow.json`);
970
1004
  console.log(` 3. npm publish`);
971
1005
  console.log();
972
1006
  } catch (err) {
@@ -1021,6 +1055,39 @@ function registerWorkflow(program) {
1021
1055
  console.log(` ${pc.dim('Run with:')} vai workflow run ${filename} --input query="your question"`);
1022
1056
  console.log(` ${pc.dim('Validate:')} vai workflow validate ${filename}`);
1023
1057
  });
1058
+
1059
+ // ── workflow clear-cache [name] ──
1060
+ wfCmd
1061
+ .command('clear-cache [name]')
1062
+ .description('Clear cached workflow inputs (from previous runs)')
1063
+ .action((name) => {
1064
+ const { clearInputCache, loadInputCache, slugify, CACHE_PATH } = require('../lib/workflow-input-cache');
1065
+ const fs = require('fs');
1066
+
1067
+ if (name) {
1068
+ const cached = loadInputCache(name);
1069
+ const keys = Object.keys(cached);
1070
+ if (keys.length === 0) {
1071
+ console.log(pc.dim(`No cached inputs for "${name}".`));
1072
+ return;
1073
+ }
1074
+ clearInputCache(name);
1075
+ console.log(ui.success(`Cleared cached inputs for "${name}" (${keys.length} field${keys.length === 1 ? '' : 's'}).`));
1076
+ } else {
1077
+ // Clear all
1078
+ let count = 0;
1079
+ try {
1080
+ const raw = fs.readFileSync(CACHE_PATH, 'utf-8');
1081
+ count = Object.keys(JSON.parse(raw)).length;
1082
+ } catch { /* no file */ }
1083
+ if (count === 0) {
1084
+ console.log(pc.dim('No cached workflow inputs.'));
1085
+ return;
1086
+ }
1087
+ clearInputCache();
1088
+ console.log(ui.success(`Cleared cached inputs for ${count} workflow${count === 1 ? '' : 's'}.`));
1089
+ }
1090
+ });
1024
1091
  }
1025
1092
 
1026
1093
  /**
@@ -39,8 +39,10 @@ const MODEL_CATALOG = [
39
39
  { name: 'voyage-4-nano', type: 'embedding', context: '32K', dimensions: '512 (default), 128, 256', price: 'Open-weight (free)', pricePerMToken: 0, bestFor: 'Open-weight / edge / local', shortFor: 'Open / edge', local: true, unreleased: true, family: 'voyage-4', architecture: 'dense', sharedSpace: 'voyage-4', huggingface: 'https://huggingface.co/voyageai/voyage-4-nano', rtebScore: null },
40
40
  // Legacy models
41
41
  { name: 'voyage-3-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', pricePerMToken: 0.18, bestFor: 'Previous gen quality', shortFor: 'Previous gen quality', legacy: true, rtebScore: null },
42
- { name: 'voyage-3.5', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.06/1M tokens', pricePerMToken: 0.06, bestFor: 'Previous gen balanced', shortFor: 'Previous gen balanced', legacy: true, rtebScore: null },
43
- { name: 'voyage-3.5-lite', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.02/1M tokens', pricePerMToken: 0.02, bestFor: 'Previous gen budget', shortFor: 'Previous gen budget', legacy: true, rtebScore: null },
42
+ { name: 'voyage-3', type: 'embedding', context: '32K', dimensions: '1024', price: '$0.06/1M tokens', pricePerMToken: 0.06, bestFor: 'Previous gen balanced', shortFor: 'Previous gen balanced', legacy: true, rtebScore: null },
43
+ { name: 'voyage-3-lite', type: 'embedding', context: '32K', dimensions: '512', price: '$0.02/1M tokens', pricePerMToken: 0.02, bestFor: 'Previous gen budget', shortFor: 'Previous gen budget', legacy: true, rtebScore: null },
44
+ { name: 'voyage-3.5', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.06/1M tokens', pricePerMToken: 0.06, bestFor: 'Previous gen balanced (3.5)', shortFor: 'Previous gen 3.5', legacy: true, rtebScore: null },
45
+ { name: 'voyage-3.5-lite', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.02/1M tokens', pricePerMToken: 0.02, bestFor: 'Previous gen budget (3.5)', shortFor: 'Previous gen 3.5-lite', legacy: true, rtebScore: null },
44
46
  { name: 'voyage-code-2', type: 'embedding', context: '16K', dimensions: '1536', price: '$0.12/1M tokens', pricePerMToken: 0.12, bestFor: 'Legacy code', shortFor: 'Legacy code', legacy: true },
45
47
  { name: 'voyage-multimodal-3', type: 'embedding-multimodal', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', pricePerMToken: 0.12, bestFor: 'Legacy multimodal', shortFor: 'Legacy multimodal', legacy: true, multimodal: true },
46
48
  { name: 'rerank-2', type: 'reranking', context: '16K', dimensions: '—', price: '$0.05/1M tokens', pricePerMToken: 0.05, bestFor: 'Legacy reranker', shortFor: 'Legacy reranker', legacy: true },