voyageai-cli 1.30.0 → 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 (82) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -1
  3. package/src/cli.js +8 -0
  4. package/src/commands/about.js +3 -3
  5. package/src/commands/chat.js +32 -11
  6. package/src/commands/code-search.js +751 -0
  7. package/src/commands/doctor.js +1 -1
  8. package/src/commands/export.js +124 -0
  9. package/src/commands/import.js +195 -0
  10. package/src/commands/index-workspace.js +243 -0
  11. package/src/commands/mcp-server.js +113 -3
  12. package/src/commands/playground.js +120 -4
  13. package/src/commands/quickstart.js +4 -4
  14. package/src/commands/workflow.js +132 -65
  15. package/src/lib/catalog.js +4 -2
  16. package/src/lib/code-search.js +315 -0
  17. package/src/lib/codegen.js +1 -1
  18. package/src/lib/explanations.js +3 -3
  19. package/src/lib/export/contexts/benchmark-export.js +27 -0
  20. package/src/lib/export/contexts/chat-export.js +41 -0
  21. package/src/lib/export/contexts/explore-export.js +22 -0
  22. package/src/lib/export/contexts/search-export.js +54 -0
  23. package/src/lib/export/contexts/workflow-export.js +80 -0
  24. package/src/lib/export/formats/clipboard-export.js +29 -0
  25. package/src/lib/export/formats/csv-export.js +45 -0
  26. package/src/lib/export/formats/json-export.js +50 -0
  27. package/src/lib/export/formats/markdown-export.js +189 -0
  28. package/src/lib/export/formats/mermaid-export.js +274 -0
  29. package/src/lib/export/formats/pdf-export.js +117 -0
  30. package/src/lib/export/formats/png-export.js +96 -0
  31. package/src/lib/export/formats/svg-export.js +116 -0
  32. package/src/lib/export/index.js +175 -0
  33. package/src/lib/github.js +226 -0
  34. package/src/lib/template-engine.js +154 -20
  35. package/src/lib/workflow-builder.js +753 -0
  36. package/src/lib/workflow-formatters.js +454 -0
  37. package/src/lib/workflow-input-cache.js +111 -0
  38. package/src/lib/workflow-scaffold.js +1 -1
  39. package/src/lib/workflow.js +297 -28
  40. package/src/mcp/install.js +280 -7
  41. package/src/mcp/schemas/index.js +170 -0
  42. package/src/mcp/server.js +19 -4
  43. package/src/mcp/tools/authoring.js +662 -0
  44. package/src/mcp/tools/code-search.js +620 -0
  45. package/src/mcp/tools/ingest.js +2 -5
  46. package/src/mcp/tools/retrieval.js +2 -15
  47. package/src/mcp/tools/workspace.js +452 -0
  48. package/src/mcp/utils.js +20 -0
  49. package/src/playground/announcements.md +52 -5
  50. package/src/playground/help/workflow-nodes.js +127 -2
  51. package/src/playground/index.html +17109 -12438
  52. package/src/playground/vendor/mermaid.min.js +2811 -0
  53. package/src/workflows/code-review.json +110 -0
  54. package/src/workflows/cost-analysis.json +5 -0
  55. package/src/workflows/rag-chat.json +165 -0
  56. package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
  57. package/src/workflows/tests/code-review.happy-path.test.json +121 -0
  58. package/src/workflows/tests/code-review.no-question.test.json +70 -0
  59. package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
  60. package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
  61. package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
  62. package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
  63. package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
  64. package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
  65. package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
  66. package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
  67. package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
  68. package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
  69. package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
  70. package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
  71. package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
  72. package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
  73. package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
  74. package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
  75. package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
  76. package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
  77. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
  78. package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -0
  79. package/src/playground/assets/announcements/appstore.jpg +0 -0
  80. package/src/playground/assets/announcements/circuits.jpg +0 -0
  81. package/src/playground/assets/announcements/csvingest.jpg +0 -0
  82. package/src/playground/assets/announcements/green-wave.jpg +0 -0
@@ -53,21 +53,35 @@ function registerMcpServer(program) {
53
53
  // Subcommand: install
54
54
  cmd
55
55
  .command('install [targets...]')
56
- .description('Install vai MCP server into AI tool configs (claude, claude-code, cursor, windsurf, vscode, or "all")')
56
+ .description('Install vai MCP server into AI tool configs (claude, claude-code, cursor, windsurf, vscode, vscode-insiders, or "all")')
57
57
  .option('--force', 'Overwrite existing vai entry', false)
58
58
  .option('--transport <mode>', 'Transport mode: stdio or http', 'stdio')
59
59
  .option('--port <number>', 'HTTP port (http transport only)', (v) => parseInt(v, 10))
60
60
  .option('--api-key <key>', 'Voyage API key to embed in config')
61
+ .option('--mongodb-uri <uri>', 'MongoDB URI to embed in config')
62
+ .option('--db <name>', 'Default database name')
63
+ .option('--collection <name>', 'Default collection name')
64
+ .option('--workspace-path <path>', 'Workspace path for workspace-level config')
65
+ .option('--verbose', 'Enable verbose MCP logging')
66
+ .option('--show-tips', 'Show integration tips after install', true)
61
67
  .action((targets, opts) => {
62
68
  const { TARGETS, installTarget } = require('../mcp/install');
63
69
 
64
70
  if (!targets.length) {
65
71
  console.log('Usage: vai mcp install <target|all>');
66
- console.log(`Available targets: ${Object.keys(TARGETS).join(', ')}, all`);
72
+ console.log(`\nAvailable targets:`);
73
+ for (const [key, target] of Object.entries(TARGETS)) {
74
+ const note = target.requiresWorkspace ? ' (workspace-level)' : '';
75
+ console.log(` ${key.padEnd(18)} ${target.name}${note}`);
76
+ }
77
+ console.log(` ${'all'.padEnd(18)} Install to all global targets`);
67
78
  return;
68
79
  }
69
80
 
70
- const keys = targets.includes('all') ? Object.keys(TARGETS) : targets;
81
+ // Filter out workspace-only targets for 'all'
82
+ const keys = targets.includes('all')
83
+ ? Object.entries(TARGETS).filter(([_, t]) => !t.requiresWorkspace).map(([k]) => k)
84
+ : targets;
71
85
 
72
86
  for (const key of keys) {
73
87
  try {
@@ -76,8 +90,22 @@ function registerMcpServer(program) {
76
90
  transport: opts.transport,
77
91
  port: opts.port,
78
92
  apiKey: opts.apiKey,
93
+ mongodbUri: opts.mongodbUri,
94
+ db: opts.db,
95
+ collection: opts.collection,
96
+ workspacePath: opts.workspacePath,
97
+ verbose: opts.verbose,
79
98
  });
80
99
  console.log(result.installed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
100
+
101
+ // Show tips if available and requested
102
+ if (result.tips && opts.showTips) {
103
+ console.log('');
104
+ for (const tip of result.tips) {
105
+ console.log(` ${tip}`);
106
+ }
107
+ console.log('');
108
+ }
81
109
  } catch (err) {
82
110
  console.error(`❌ ${key}: ${err.message}`);
83
111
  }
@@ -123,6 +151,88 @@ function registerMcpServer(program) {
123
151
  }
124
152
  console.log('');
125
153
  });
154
+
155
+ // Subcommand: diagnose
156
+ cmd
157
+ .command('diagnose [target]')
158
+ .description('Diagnose MCP installation issues for a specific target')
159
+ .action((target) => {
160
+ const { diagnose, TARGETS } = require('../mcp/install');
161
+
162
+ if (!target) {
163
+ console.log('Usage: vai mcp diagnose <target>');
164
+ console.log(`Available targets: ${Object.keys(TARGETS).join(', ')}`);
165
+ return;
166
+ }
167
+
168
+ console.log(`\nvai MCP Diagnostics — ${target}\n`);
169
+
170
+ const results = diagnose(target);
171
+ for (const r of results) {
172
+ const icon = r.level === 'ok' ? '✅' : r.level === 'warning' ? '⚠️ ' : '❌';
173
+ console.log(` ${icon} ${r.message}`);
174
+ }
175
+ console.log('');
176
+ });
177
+
178
+ // Subcommand: sample-config
179
+ cmd
180
+ .command('sample-config <target>')
181
+ .description('Generate sample MCP config for a target (cursor, vscode, etc.)')
182
+ .option('--transport <mode>', 'Transport mode: stdio or http', 'stdio')
183
+ .option('--port <number>', 'HTTP port', (v) => parseInt(v, 10))
184
+ .action((target, opts) => {
185
+ const { generateSampleConfig, TARGETS } = require('../mcp/install');
186
+
187
+ if (!TARGETS[target]) {
188
+ console.error(`Unknown target: ${target}. Available: ${Object.keys(TARGETS).join(', ')}`);
189
+ process.exit(1);
190
+ }
191
+
192
+ const sample = generateSampleConfig(target, {
193
+ transport: opts.transport,
194
+ port: opts.port,
195
+ });
196
+
197
+ if (!sample) {
198
+ console.error('Failed to generate sample config');
199
+ process.exit(1);
200
+ }
201
+
202
+ console.log(`\n# Sample MCP config for ${TARGETS[target].name}`);
203
+ console.log(`# Add this to your config file:\n`);
204
+ console.log(sample);
205
+ });
206
+
207
+ // Subcommand: info
208
+ cmd
209
+ .command('info <target>')
210
+ .description('Show detailed information about a target')
211
+ .action((target) => {
212
+ const { getTargetInfo } = require('../mcp/install');
213
+
214
+ const info = getTargetInfo(target);
215
+ if (!info) {
216
+ console.error(`Unknown target: ${target}`);
217
+ process.exit(1);
218
+ }
219
+
220
+ console.log(`\n${info.name}\n`);
221
+ console.log(` Config path: ${info.configPath || 'N/A (workspace-level only)'}`);
222
+ if (info.workspaceConfigPath) {
223
+ console.log(` Workspace path: ${info.workspaceConfigPath}`);
224
+ }
225
+ if (info.requiresWorkspace) {
226
+ console.log(` Note: Requires --workspace-path option`);
227
+ }
228
+ if (info.tips.length > 0) {
229
+ console.log(`\n Tips:`);
230
+ for (const tip of info.tips) {
231
+ console.log(` ${tip}`);
232
+ }
233
+ }
234
+ console.log('');
235
+ });
126
236
  }
127
237
 
128
238
  module.exports = { registerMcpServer };
@@ -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
 
@@ -247,6 +247,24 @@ function createPlaygroundServer() {
247
247
  return;
248
248
  }
249
249
 
250
+ // Serve vendor assets (bundled JS libraries)
251
+ const vendorMatch = req.url.match(/^\/vendor\/([a-zA-Z0-9_.-]+\.js)$/);
252
+ if (req.method === 'GET' && vendorMatch) {
253
+ const vendorPath = path.join(__dirname, '..', 'playground', 'vendor', vendorMatch[1]);
254
+ if (fs.existsSync(vendorPath)) {
255
+ const data = fs.readFileSync(vendorPath);
256
+ res.writeHead(200, {
257
+ 'Content-Type': 'application/javascript; charset=utf-8',
258
+ 'Cache-Control': 'public, max-age=86400',
259
+ });
260
+ res.end(data);
261
+ } else {
262
+ res.writeHead(404);
263
+ res.end('Vendor asset not found');
264
+ }
265
+ return;
266
+ }
267
+
250
268
  // API: Models
251
269
  if (req.method === 'GET' && req.url === '/api/models') {
252
270
  const models = MODEL_CATALOG.filter(m => !m.legacy && !m.local && !m.unreleased);
@@ -255,6 +273,13 @@ function createPlaygroundServer() {
255
273
  return;
256
274
  }
257
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
+
258
283
  // API: Generate code
259
284
  if (req.method === 'POST' && req.url === '/api/generate') {
260
285
  let body = '';
@@ -460,6 +485,62 @@ function createPlaygroundServer() {
460
485
  return;
461
486
  }
462
487
 
488
+ // API: MCP status — installation status across all tools
489
+ if (req.method === 'GET' && req.url === '/api/mcp/status') {
490
+ try {
491
+ const { statusAll } = require('../mcp/install');
492
+ const results = statusAll();
493
+ res.writeHead(200, { 'Content-Type': 'application/json' });
494
+ res.end(JSON.stringify(results));
495
+ } catch (err) {
496
+ res.writeHead(500, { 'Content-Type': 'application/json' });
497
+ res.end(JSON.stringify({ error: err.message }));
498
+ }
499
+ return;
500
+ }
501
+
502
+ // API: MCP install — install vai into a target tool
503
+ if (req.method === 'POST' && req.url === '/api/mcp/install') {
504
+ try {
505
+ const body = await readBody(req);
506
+ const { target, force } = JSON.parse(body);
507
+ if (!target) {
508
+ res.writeHead(400, { 'Content-Type': 'application/json' });
509
+ res.end(JSON.stringify({ error: 'target is required' }));
510
+ return;
511
+ }
512
+ const { installTarget } = require('../mcp/install');
513
+ const result = installTarget(target, { force: force || false });
514
+ res.writeHead(200, { 'Content-Type': 'application/json' });
515
+ res.end(JSON.stringify(result));
516
+ } catch (err) {
517
+ res.writeHead(500, { 'Content-Type': 'application/json' });
518
+ res.end(JSON.stringify({ error: err.message }));
519
+ }
520
+ return;
521
+ }
522
+
523
+ // API: MCP uninstall — remove vai from a target tool
524
+ if (req.method === 'POST' && req.url === '/api/mcp/uninstall') {
525
+ try {
526
+ const body = await readBody(req);
527
+ const { target } = JSON.parse(body);
528
+ if (!target) {
529
+ res.writeHead(400, { 'Content-Type': 'application/json' });
530
+ res.end(JSON.stringify({ error: 'target is required' }));
531
+ return;
532
+ }
533
+ const { uninstallTarget } = require('../mcp/install');
534
+ const result = uninstallTarget(target);
535
+ res.writeHead(200, { 'Content-Type': 'application/json' });
536
+ res.end(JSON.stringify(result));
537
+ } catch (err) {
538
+ res.writeHead(500, { 'Content-Type': 'application/json' });
539
+ res.end(JSON.stringify({ error: err.message }));
540
+ }
541
+ return;
542
+ }
543
+
463
544
  // API: Settings origins — where each config value comes from
464
545
  if (req.method === 'GET' && req.url === '/api/settings/origins') {
465
546
  const { resolveLLMConfig } = require('../lib/llm');
@@ -1420,15 +1501,23 @@ function createPlaygroundServer() {
1420
1501
  // API: Validate a workflow definition
1421
1502
  if (req.url === '/api/workflows/validate') {
1422
1503
  const { validateWorkflow } = require('../lib/workflow');
1423
- const { definition } = parsed;
1504
+ const { definition, mode } = parsed;
1424
1505
  if (!definition) {
1425
1506
  res.writeHead(400, { 'Content-Type': 'application/json' });
1426
1507
  res.end(JSON.stringify({ error: 'definition is required' }));
1427
1508
  return;
1428
1509
  }
1429
- const errors = validateWorkflow(definition);
1510
+ const validationMode = mode || 'strict';
1511
+ const result = validateWorkflow(definition, { mode: validationMode });
1512
+
1430
1513
  res.writeHead(200, { 'Content-Type': 'application/json' });
1431
- res.end(JSON.stringify({ valid: errors.length === 0, errors }));
1514
+
1515
+ if (validationMode === 'draft') {
1516
+ res.end(JSON.stringify(result));
1517
+ } else {
1518
+ // Backward compatible format for strict mode
1519
+ res.end(JSON.stringify({ valid: result.length === 0, errors: result }));
1520
+ }
1432
1521
  return;
1433
1522
  }
1434
1523
 
@@ -1522,6 +1611,7 @@ function createPlaygroundServer() {
1522
1611
  totalTimeMs: result.totalTimeMs,
1523
1612
  layers: result.layers,
1524
1613
  steps: result.steps,
1614
+ formatters: result.formatters || null,
1525
1615
  })}\n\n`);
1526
1616
  } catch (err) {
1527
1617
  res.write(`event: error\ndata: ${JSON.stringify({ error: err.message })}\n\n`);
@@ -1532,6 +1622,32 @@ function createPlaygroundServer() {
1532
1622
  }
1533
1623
  }
1534
1624
 
1625
+ // ── Export API endpoints ──
1626
+ const exportMatch = req.url.match(/^\/api\/export\/(workflow|chat|search|benchmark)$/);
1627
+ if (req.method === 'POST' && exportMatch) {
1628
+ const context = exportMatch[1];
1629
+ try {
1630
+ const body = JSON.parse(await readBody(req));
1631
+ const { exportArtifact } = require('../lib/export');
1632
+ const result = await exportArtifact({
1633
+ context,
1634
+ format: body.format || 'json',
1635
+ data: body.data || {},
1636
+ options: body.options || {},
1637
+ });
1638
+ const isBinary = Buffer.isBuffer(result.content);
1639
+ res.writeHead(200, {
1640
+ 'Content-Type': result.mimeType,
1641
+ 'Content-Disposition': `attachment; filename="${result.suggestedFilename}"`,
1642
+ });
1643
+ res.end(isBinary ? result.content : result.content);
1644
+ } catch (err) {
1645
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1646
+ res.end(JSON.stringify({ error: err.message }));
1647
+ }
1648
+ return;
1649
+ }
1650
+
1535
1651
  // 404
1536
1652
  res.writeHead(404, { 'Content-Type': 'application/json' });
1537
1653
  res.end(JSON.stringify({ error: 'Not found' }));
@@ -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 },