voyageai-cli 1.28.0 → 1.30.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 (58) hide show
  1. package/README.md +82 -8
  2. package/package.json +2 -1
  3. package/src/commands/app.js +15 -0
  4. package/src/commands/benchmark.js +22 -8
  5. package/src/commands/chat.js +18 -0
  6. package/src/commands/chunk.js +10 -0
  7. package/src/commands/demo.js +4 -0
  8. package/src/commands/embed.js +13 -0
  9. package/src/commands/estimate.js +3 -0
  10. package/src/commands/eval.js +6 -0
  11. package/src/commands/explain.js +2 -0
  12. package/src/commands/generate.js +2 -0
  13. package/src/commands/ingest.js +4 -0
  14. package/src/commands/init.js +2 -0
  15. package/src/commands/mcp-server.js +2 -0
  16. package/src/commands/models.js +2 -0
  17. package/src/commands/ping.js +7 -0
  18. package/src/commands/pipeline.js +15 -0
  19. package/src/commands/playground.js +685 -8
  20. package/src/commands/query.js +16 -0
  21. package/src/commands/rerank.js +12 -0
  22. package/src/commands/scaffold.js +2 -0
  23. package/src/commands/search.js +11 -0
  24. package/src/commands/similarity.js +9 -0
  25. package/src/commands/store.js +4 -0
  26. package/src/commands/workflow.js +702 -13
  27. package/src/lib/capability-report.js +134 -0
  28. package/src/lib/chat.js +32 -1
  29. package/src/lib/config.js +2 -0
  30. package/src/lib/cost-display.js +107 -0
  31. package/src/lib/explanations.js +94 -0
  32. package/src/lib/llm.js +125 -18
  33. package/src/lib/npm-utils.js +265 -0
  34. package/src/lib/quality-audit.js +71 -0
  35. package/src/lib/security/blocked-domains.json +17 -0
  36. package/src/lib/security-audit.js +198 -0
  37. package/src/lib/telemetry.js +23 -1
  38. package/src/lib/workflow-registry.js +416 -0
  39. package/src/lib/workflow-scaffold.js +380 -0
  40. package/src/lib/workflow-test-runner.js +208 -0
  41. package/src/lib/workflow.js +559 -7
  42. package/src/playground/announcements.md +80 -0
  43. package/src/playground/assets/announcements/appstore.jpg +0 -0
  44. package/src/playground/assets/announcements/circuits.jpg +0 -0
  45. package/src/playground/assets/announcements/csvingest.jpg +0 -0
  46. package/src/playground/assets/announcements/green-wave.jpg +0 -0
  47. package/src/playground/help/workflow-nodes.js +472 -0
  48. package/src/playground/icons/V.png +0 -0
  49. package/src/playground/index.html +3634 -226
  50. package/src/workflows/consistency-check.json +4 -0
  51. package/src/workflows/cost-analysis.json +4 -0
  52. package/src/workflows/enrich-and-ingest.json +56 -0
  53. package/src/workflows/intelligent-ingest.json +66 -0
  54. package/src/workflows/kb-health-report.json +45 -0
  55. package/src/workflows/multi-collection-search.json +4 -0
  56. package/src/workflows/research-and-summarize.json +4 -0
  57. package/src/workflows/search-with-fallback.json +66 -0
  58. package/src/workflows/smart-ingest.json +4 -0
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ const CAP_ICONS = {
4
+ NETWORK: '🌐',
5
+ WRITE_DB: '💾',
6
+ LLM: '🤖',
7
+ LOOP: '🔄',
8
+ READ_DB: '📊',
9
+ };
10
+
11
+ const SEVERITY_ICONS = {
12
+ critical: '🔴',
13
+ high: '🟠',
14
+ medium: '🟡',
15
+ low: '🔵',
16
+ };
17
+
18
+ /**
19
+ * Generate a markdown-formatted capability report for a workflow package.
20
+ *
21
+ * @param {object} definition - Parsed workflow JSON
22
+ * @param {Array<{severity: string, message: string, stepId?: string}>} securityFindings
23
+ * @param {Array<{level: string, message: string}>} qualityIssues
24
+ * @param {{ total: number, passed: number, failed: number, results?: Array }} [testResults]
25
+ * @returns {string} Markdown-formatted report
26
+ */
27
+ function generateCapabilityReport(definition, securityFindings, qualityIssues, testResults) {
28
+ const lines = [];
29
+
30
+ const name = definition?.name || 'Unknown Workflow';
31
+ lines.push(`## 📋 Workflow Validation Report: ${name}`);
32
+ lines.push('');
33
+
34
+ // ── Capabilities ──
35
+ const { extractCapabilities } = require('./security-audit');
36
+ const caps = definition ? [...extractCapabilities(definition)] : [];
37
+
38
+ lines.push('### Capabilities');
39
+ if (caps.length === 0) {
40
+ lines.push('No special capabilities detected.');
41
+ } else {
42
+ for (const cap of caps) {
43
+ lines.push(`- ${CAP_ICONS[cap] || '•'} **${cap}**`);
44
+ }
45
+ }
46
+ lines.push('');
47
+
48
+ // ── Security Findings ──
49
+ lines.push('### Security Audit');
50
+ if (!securityFindings || securityFindings.length === 0) {
51
+ lines.push('✅ No security issues found.');
52
+ } else {
53
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
54
+ for (const f of securityFindings) {
55
+ counts[f.severity] = (counts[f.severity] || 0) + 1;
56
+ }
57
+ const summary = Object.entries(counts)
58
+ .filter(([, v]) => v > 0)
59
+ .map(([k, v]) => `${SEVERITY_ICONS[k]} ${v} ${k.toUpperCase()}`)
60
+ .join(' | ');
61
+ lines.push(summary);
62
+ lines.push('');
63
+ lines.push('| Severity | Finding | Step |');
64
+ lines.push('|----------|---------|------|');
65
+ for (const f of securityFindings) {
66
+ lines.push(`| ${SEVERITY_ICONS[f.severity]} ${f.severity.toUpperCase()} | ${f.message} | ${f.stepId || '—'} |`);
67
+ }
68
+ }
69
+ lines.push('');
70
+
71
+ // ── Quality ──
72
+ lines.push('### Quality Audit');
73
+ if (!qualityIssues || qualityIssues.length === 0) {
74
+ lines.push('✅ No quality issues found.');
75
+ } else {
76
+ const errorCount = qualityIssues.filter(i => i.level === 'error').length;
77
+ const warningCount = qualityIssues.filter(i => i.level === 'warning').length;
78
+ const suggestionCount = qualityIssues.filter(i => i.level === 'suggestion').length;
79
+
80
+ const parts = [];
81
+ if (errorCount) parts.push(`❌ ${errorCount} error(s)`);
82
+ if (warningCount) parts.push(`⚠️ ${warningCount} warning(s)`);
83
+ if (suggestionCount) parts.push(`💡 ${suggestionCount} suggestion(s)`);
84
+ lines.push(parts.join(' | '));
85
+ lines.push('');
86
+
87
+ for (const issue of qualityIssues) {
88
+ const icon = issue.level === 'error' ? '❌' : issue.level === 'warning' ? '⚠️' : '💡';
89
+ lines.push(`- ${icon} **[${issue.level.toUpperCase()}]** ${issue.message}`);
90
+ }
91
+ }
92
+ lines.push('');
93
+
94
+ // ── Test Results ──
95
+ lines.push('### Test Results');
96
+ if (!testResults) {
97
+ lines.push('⏭️ No test results available.');
98
+ } else if (testResults.total === 0) {
99
+ lines.push('⏭️ No test cases found.');
100
+ } else {
101
+ const status = testResults.failed === 0 ? '✅' : '❌';
102
+ lines.push(`${status} **${testResults.passed}/${testResults.total}** tests passed`);
103
+ if (testResults.results && testResults.results.length > 0) {
104
+ lines.push('');
105
+ for (const r of testResults.results) {
106
+ const icon = r.passed ? '✅' : '❌';
107
+ lines.push(`- ${icon} ${r.name || r.file}`);
108
+ }
109
+ }
110
+ }
111
+ lines.push('');
112
+
113
+ // ── Overall Summary ──
114
+ const criticalCount = (securityFindings || []).filter(f => f.severity === 'critical').length;
115
+ const highCount = (securityFindings || []).filter(f => f.severity === 'high').length;
116
+ const qualityErrors = (qualityIssues || []).filter(i => i.level === 'error').length;
117
+ const testsFailed = testResults ? testResults.failed : 0;
118
+
119
+ lines.push('### Summary');
120
+ if (criticalCount === 0 && highCount === 0 && qualityErrors === 0 && testsFailed === 0) {
121
+ lines.push('✅ **All checks passed.** This workflow is ready for review.');
122
+ } else {
123
+ const issues = [];
124
+ if (criticalCount) issues.push(`${criticalCount} critical security finding(s)`);
125
+ if (highCount) issues.push(`${highCount} high security finding(s)`);
126
+ if (qualityErrors) issues.push(`${qualityErrors} quality error(s)`);
127
+ if (testsFailed) issues.push(`${testsFailed} test failure(s)`);
128
+ lines.push(`⚠️ **Issues found:** ${issues.join(', ')}`);
129
+ }
130
+
131
+ return lines.join('\n');
132
+ }
133
+
134
+ module.exports = { generateCapabilityReport };
package/src/lib/chat.js CHANGED
@@ -202,9 +202,15 @@ async function* chatTurn({ query, db, collection, llm, history, opts = {} }) {
202
202
  // 3. Generate response (streaming)
203
203
  let fullResponse = '';
204
204
  const stream = opts.stream !== false;
205
+ let llmUsage = { inputTokens: 0, outputTokens: 0 };
205
206
 
206
207
  try {
207
208
  for await (const chunk of llm.chat(messages, { stream })) {
209
+ // Check for __usage sentinel (yielded as final item from LLM providers)
210
+ if (typeof chunk === 'object' && chunk !== null && chunk.__usage) {
211
+ llmUsage = chunk.__usage;
212
+ continue;
213
+ }
208
214
  fullResponse += chunk;
209
215
  yield { type: 'chunk', data: chunk };
210
216
  }
@@ -240,7 +246,13 @@ async function* chatTurn({ query, db, collection, llm, history, opts = {} }) {
240
246
  metadata: {
241
247
  retrievalTimeMs,
242
248
  generationTimeMs,
243
- tokens,
249
+ tokens: {
250
+ ...tokens,
251
+ llmInput: llmUsage.inputTokens,
252
+ llmOutput: llmUsage.outputTokens,
253
+ },
254
+ llmModel: llm.model,
255
+ llmProvider: llm.name,
244
256
  contextDocsUsed: docs.length,
245
257
  },
246
258
  },
@@ -284,11 +296,18 @@ async function* agentChatTurn({ query, llm, history, opts = {} }) {
284
296
  // Track messages for the tool-calling loop (mutable copy)
285
297
  const messages = [...initialMessages];
286
298
  const toolCallLog = [];
299
+ const totalLlmUsage = { inputTokens: 0, outputTokens: 0 };
287
300
 
288
301
  // 3. Agent loop
289
302
  for (let iteration = 0; iteration < maxIterations; iteration++) {
290
303
  const response = await llm.chatWithTools(messages, tools);
291
304
 
305
+ // Accumulate LLM usage from each chatWithTools call
306
+ if (response.usage) {
307
+ totalLlmUsage.inputTokens += response.usage.inputTokens || 0;
308
+ totalLlmUsage.outputTokens += response.usage.outputTokens || 0;
309
+ }
310
+
292
311
  // Text response: done
293
312
  if (response.type === 'text') {
294
313
  const fullResponse = response.content;
@@ -321,6 +340,12 @@ async function* agentChatTurn({ query, llm, history, opts = {} }) {
321
340
  iterationCount: iteration + 1,
322
341
  toolCallCount: toolCallLog.length,
323
342
  totalTimeMs,
343
+ tokens: {
344
+ llmInput: totalLlmUsage.inputTokens,
345
+ llmOutput: totalLlmUsage.outputTokens,
346
+ },
347
+ llmModel: llm.model,
348
+ llmProvider: llm.name,
324
349
  },
325
350
  },
326
351
  };
@@ -406,6 +431,12 @@ async function* agentChatTurn({ query, llm, history, opts = {} }) {
406
431
  toolCallCount: toolCallLog.length,
407
432
  totalTimeMs: Date.now() - start,
408
433
  maxIterationsReached: true,
434
+ tokens: {
435
+ llmInput: totalLlmUsage.inputTokens,
436
+ llmOutput: totalLlmUsage.outputTokens,
437
+ },
438
+ llmModel: llm.model,
439
+ llmProvider: llm.name,
409
440
  },
410
441
  },
411
442
  };
package/src/lib/config.js CHANGED
@@ -18,6 +18,8 @@ const KEY_MAP = {
18
18
  'llm-api-key': 'llmApiKey',
19
19
  'llm-model': 'llmModel',
20
20
  'llm-base-url': 'llmBaseUrl',
21
+ 'show-cost': 'show-cost',
22
+ 'telemetry': 'telemetry',
21
23
  };
22
24
 
23
25
  // Keys whose values should be masked in output
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const { MODEL_CATALOG } = require('./catalog');
4
+ const { getConfigValue } = require('./config');
5
+ const ui = require('./ui');
6
+
7
+ const COMPETITOR_PRICE = 0.13; // OpenAI text-embedding-3-large per 1M tokens
8
+ const LARGE_PRICE = 0.12; // voyage-4-large per 1M tokens
9
+
10
+ /**
11
+ * Show a one-line cost summary after a CLI operation.
12
+ * Only displays when `show-cost` config is enabled.
13
+ * Respects --json and --quiet flags.
14
+ *
15
+ * @param {string} model - Model name used
16
+ * @param {number} tokens - Total tokens consumed
17
+ * @param {object} [opts] - Command options
18
+ * @param {boolean} [opts.json] - JSON output mode (suppress cost)
19
+ * @param {boolean} [opts.quiet] - Quiet mode (suppress cost)
20
+ */
21
+ function showCostSummary(model, tokens, opts = {}) {
22
+ if (opts.json || opts.quiet) return;
23
+ if (!isEnabled()) return;
24
+ if (!tokens || tokens <= 0) return;
25
+
26
+ const entry = MODEL_CATALOG.find(m => m.name === model);
27
+ const price = entry?.pricePerMToken ?? LARGE_PRICE;
28
+ const cost = (tokens / 1_000_000) * price;
29
+ const largeCost = (tokens / 1_000_000) * LARGE_PRICE;
30
+
31
+ const costStr = formatCost(cost);
32
+ const tokStr = tokens.toLocaleString();
33
+
34
+ console.log();
35
+ console.log(ui.dim(` 💰 ${costStr} (${tokStr} tokens, ${model})`));
36
+
37
+ if (price < LARGE_PRICE) {
38
+ const savingsPercent = Math.round((1 - price / LARGE_PRICE) * 100);
39
+ const largeStr = formatCost(largeCost);
40
+ console.log(ui.dim(` Symmetric (voyage-4-large): ${largeStr} — ${savingsPercent}% savings`));
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Show a combined cost summary for operations with multiple API calls
46
+ * (e.g., query with embed + rerank).
47
+ *
48
+ * @param {Array<{model: string, tokens: number, label?: string}>} operations
49
+ * @param {object} [opts]
50
+ */
51
+ function showCombinedCostSummary(operations, opts = {}) {
52
+ if (opts.json || opts.quiet) return;
53
+ if (!isEnabled()) return;
54
+
55
+ let totalCost = 0;
56
+ let totalLargeCost = 0;
57
+ let totalTokens = 0;
58
+
59
+ for (const op of operations) {
60
+ if (!op.tokens || op.tokens <= 0) continue;
61
+ const entry = MODEL_CATALOG.find(m => m.name === op.model);
62
+ const price = entry?.pricePerMToken ?? LARGE_PRICE;
63
+ totalCost += (op.tokens / 1_000_000) * price;
64
+ totalLargeCost += (op.tokens / 1_000_000) * LARGE_PRICE;
65
+ totalTokens += op.tokens;
66
+ }
67
+
68
+ if (totalTokens <= 0) return;
69
+
70
+ console.log();
71
+ console.log(ui.dim(` 💰 ${formatCost(totalCost)} total (${totalTokens.toLocaleString()} tokens)`));
72
+ for (const op of operations) {
73
+ if (!op.tokens || op.tokens <= 0) continue;
74
+ const entry = MODEL_CATALOG.find(m => m.name === op.model);
75
+ const price = entry?.pricePerMToken ?? LARGE_PRICE;
76
+ const cost = (op.tokens / 1_000_000) * price;
77
+ const label = op.label || op.model;
78
+ console.log(ui.dim(` ${label}: ${formatCost(cost)} (${op.tokens.toLocaleString()} tokens)`));
79
+ }
80
+
81
+ if (totalCost < totalLargeCost) {
82
+ const savingsPercent = Math.round((1 - totalCost / totalLargeCost) * 100);
83
+ console.log(ui.dim(` Symmetric (all voyage-4-large): ${formatCost(totalLargeCost)} — ${savingsPercent}% savings`));
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Check if cost display is enabled via config.
89
+ * @returns {boolean}
90
+ */
91
+ function isEnabled() {
92
+ const val = getConfigValue('show-cost');
93
+ return val === true || val === 'true';
94
+ }
95
+
96
+ /**
97
+ * Format a cost value for display.
98
+ * @param {number} cost
99
+ * @returns {string}
100
+ */
101
+ function formatCost(cost) {
102
+ if (cost < 0.000001) return '$0.000000';
103
+ if (cost < 0.01) return `$${cost.toFixed(6)}`;
104
+ return `$${cost.toFixed(4)}`;
105
+ }
106
+
107
+ module.exports = { showCostSummary, showCombinedCostSummary, isEnabled, formatCost };
@@ -589,9 +589,15 @@ const concepts = {
589
589
  ``,
590
590
  `${pc.bold('Validate it yourself:')} Use ${pc.cyan('vai benchmark space')} to embed identical text`,
591
591
  `with all Voyage 4 models and see the cross-model cosine similarities.`,
592
+ ``,
593
+ `${pc.bold('Interactive proof:')} Try the ${pc.cyan('Shared Space Explorer')} at`,
594
+ `${pc.cyan('vaicli.com/shared-space')} — embed text with all three models simultaneously`,
595
+ `and see 0.95+ cross-model similarity in a live 3×3 matrix, scatter plot, and`,
596
+ `cost comparison. Share your results directly to LinkedIn.`,
592
597
  ].join('\n'),
593
598
  links: [
594
599
  'https://blog.voyageai.com/2026/01/15/voyage-4-model-family/',
600
+ 'https://vaicli.com/shared-space',
595
601
  ],
596
602
  tryIt: [
597
603
  'vai benchmark space',
@@ -1478,6 +1484,87 @@ const concepts = {
1478
1484
  'vai workflow run multi-collection-search --input query="test" --dry-run',
1479
1485
  ],
1480
1486
  },
1487
+
1488
+ 'workflow-publishing': {
1489
+ title: 'Publishing vai Workflows to npm',
1490
+ summary: 'Share workflows as npm packages using the vai-workflow-* convention',
1491
+ content: [
1492
+ `${pc.bold('WHAT IS A WORKFLOW PACKAGE?')}`,
1493
+ ``,
1494
+ `A vai workflow package is an npm package containing a reusable workflow`,
1495
+ `definition. It follows the naming convention ${pc.cyan('vai-workflow-<name>')} and contains:`,
1496
+ ``,
1497
+ ` • ${pc.cyan('workflow.json')} — The workflow definition (same format as any vai workflow)`,
1498
+ ` • ${pc.cyan('package.json')} — npm metadata + vai-specific fields`,
1499
+ ` • ${pc.cyan('README.md')} — Usage instructions`,
1500
+ ``,
1501
+ `No JavaScript, no build step, no dependencies.`,
1502
+ ``,
1503
+ `${pc.bold('HOW TO PUBLISH')}`,
1504
+ ``,
1505
+ `Option 1: Package an existing workflow`,
1506
+ ``,
1507
+ ` ${pc.cyan('vai workflow create --from my-workflow.json --name my-workflow')}`,
1508
+ ``,
1509
+ ` This creates a ${pc.cyan('vai-workflow-my-workflow/')} directory with everything needed.`,
1510
+ ` Review it, then:`,
1511
+ ``,
1512
+ ` ${pc.cyan('cd vai-workflow-my-workflow')}`,
1513
+ ` ${pc.cyan('npm publish')}`,
1514
+ ``,
1515
+ `Option 2: Start from scratch`,
1516
+ ``,
1517
+ ` ${pc.cyan('vai workflow create --name my-workflow')}`,
1518
+ ``,
1519
+ ` This creates an interactive template you can fill in.`,
1520
+ ``,
1521
+ `${pc.bold('PACKAGE.JSON REQUIREMENTS')}`,
1522
+ ``,
1523
+ `Your package.json needs a ${pc.cyan('"vai"')} field with:`,
1524
+ ``,
1525
+ ` {`,
1526
+ ` "vai": {`,
1527
+ ` "workflowVersion": "1.0",`,
1528
+ ` "tools": ["query", "rerank"],`,
1529
+ ` "category": "retrieval"`,
1530
+ ` }`,
1531
+ ` }`,
1532
+ ``,
1533
+ `${pc.cyan('vai workflow create')} fills this in automatically.`,
1534
+ ``,
1535
+ `${pc.bold('NAMING')}`,
1536
+ ``,
1537
+ `Package names must start with ${pc.cyan('vai-workflow-')} followed by a lowercase, hyphenated name:`,
1538
+ ``,
1539
+ ` ${pc.green('✓')} vai-workflow-legal-research`,
1540
+ ` ${pc.green('✓')} vai-workflow-code-review`,
1541
+ ` ${pc.red('✗')} vai-legal-workflow (wrong prefix)`,
1542
+ ``,
1543
+ `${pc.bold('CATEGORIES')}`,
1544
+ ``,
1545
+ ` ${pc.cyan('retrieval')} — Search and retrieval pipelines`,
1546
+ ` ${pc.cyan('analysis')} — Comparison, consistency checking`,
1547
+ ` ${pc.cyan('ingestion')} — Document processing and storage`,
1548
+ ` ${pc.cyan('domain-specific')} — Industry-focused (legal, clinical, etc.)`,
1549
+ ` ${pc.cyan('utility')} — Cost estimation, benchmarking`,
1550
+ ` ${pc.cyan('integration')} — Multi-system workflows`,
1551
+ ``,
1552
+ `${pc.bold('TESTING BEFORE PUBLISHING')}`,
1553
+ ``,
1554
+ ` ${pc.cyan('vai workflow validate ./vai-workflow-my-workflow/workflow.json')}`,
1555
+ ` ${pc.cyan('npm install ./vai-workflow-my-workflow')}`,
1556
+ ` ${pc.cyan('vai workflow run vai-workflow-my-workflow --dry-run')}`,
1557
+ ].join('\n'),
1558
+ links: [
1559
+ 'https://github.com/mrlynn/voyageai-cli',
1560
+ ],
1561
+ tryIt: [
1562
+ 'vai workflow create --from my-pipeline.json --name my-pipeline',
1563
+ 'vai workflow create',
1564
+ 'vai workflow search legal',
1565
+ 'vai workflow install vai-workflow-legal-research',
1566
+ ],
1567
+ },
1481
1568
  };
1482
1569
 
1483
1570
  /**
@@ -1651,6 +1738,13 @@ const aliases = {
1651
1738
  'vai-workflow': 'workflows',
1652
1739
  pipeline: 'workflows',
1653
1740
  dag: 'workflows',
1741
+ // Workflow publishing aliases
1742
+ 'workflow-publishing': 'workflow-publishing',
1743
+ 'publish-workflow': 'workflow-publishing',
1744
+ 'workflow-registry': 'workflow-publishing',
1745
+ 'community-workflows': 'workflow-publishing',
1746
+ 'share-workflow': 'workflow-publishing',
1747
+ 'npm-workflow': 'workflow-publishing',
1654
1748
  };
1655
1749
 
1656
1750
  /**