voyageai-cli 1.29.0 → 1.30.1

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 (85) hide show
  1. package/README.md +82 -8
  2. package/package.json +1 -1
  3. package/src/cli.js +6 -0
  4. package/src/commands/benchmark.js +22 -8
  5. package/src/commands/chat.js +50 -11
  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/export.js +124 -0
  13. package/src/commands/generate.js +2 -0
  14. package/src/commands/import.js +195 -0
  15. package/src/commands/index-workspace.js +239 -0
  16. package/src/commands/ingest.js +4 -0
  17. package/src/commands/init.js +2 -0
  18. package/src/commands/mcp-server.js +115 -3
  19. package/src/commands/models.js +2 -0
  20. package/src/commands/ping.js +7 -0
  21. package/src/commands/pipeline.js +15 -0
  22. package/src/commands/playground.js +163 -9
  23. package/src/commands/query.js +16 -0
  24. package/src/commands/rerank.js +12 -0
  25. package/src/commands/scaffold.js +2 -0
  26. package/src/commands/search.js +11 -0
  27. package/src/commands/similarity.js +9 -0
  28. package/src/commands/store.js +4 -0
  29. package/src/commands/workflow.js +286 -0
  30. package/src/lib/capability-report.js +134 -0
  31. package/src/lib/chat.js +32 -1
  32. package/src/lib/config.js +2 -0
  33. package/src/lib/cost-display.js +107 -0
  34. package/src/lib/explanations.js +6 -0
  35. package/src/lib/export/contexts/benchmark-export.js +27 -0
  36. package/src/lib/export/contexts/chat-export.js +41 -0
  37. package/src/lib/export/contexts/explore-export.js +22 -0
  38. package/src/lib/export/contexts/search-export.js +54 -0
  39. package/src/lib/export/contexts/workflow-export.js +80 -0
  40. package/src/lib/export/formats/clipboard-export.js +29 -0
  41. package/src/lib/export/formats/csv-export.js +45 -0
  42. package/src/lib/export/formats/json-export.js +50 -0
  43. package/src/lib/export/formats/markdown-export.js +189 -0
  44. package/src/lib/export/formats/mermaid-export.js +274 -0
  45. package/src/lib/export/formats/pdf-export.js +117 -0
  46. package/src/lib/export/formats/png-export.js +96 -0
  47. package/src/lib/export/formats/svg-export.js +116 -0
  48. package/src/lib/export/index.js +175 -0
  49. package/src/lib/llm.js +125 -18
  50. package/src/lib/quality-audit.js +71 -0
  51. package/src/lib/security/blocked-domains.json +17 -0
  52. package/src/lib/security-audit.js +198 -0
  53. package/src/lib/telemetry.js +23 -1
  54. package/src/lib/workflow-scaffold.js +61 -0
  55. package/src/lib/workflow-test-runner.js +208 -0
  56. package/src/lib/workflow.js +333 -28
  57. package/src/mcp/install.js +280 -7
  58. package/src/mcp/schemas/index.js +40 -0
  59. package/src/mcp/server.js +2 -0
  60. package/src/mcp/tools/workspace.js +463 -0
  61. package/src/playground/announcements.md +56 -0
  62. package/src/playground/help/workflow-nodes.js +472 -0
  63. package/src/playground/index.html +13134 -8507
  64. package/src/playground/vendor/mermaid.min.js +2811 -0
  65. package/src/workflows/rag-chat.json +165 -0
  66. package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
  67. package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
  68. package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
  69. package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
  70. package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
  71. package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
  72. package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
  73. package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
  74. package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
  75. package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
  76. package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
  77. package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
  78. package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
  79. package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
  80. package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
  81. package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
  82. package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
  83. package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
  84. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
  85. package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -0
@@ -67,6 +67,9 @@ function loadAnnouncementsFromMarkdown() {
67
67
  badge: metadata.badge || 'Info',
68
68
  published: metadata.published || new Date().toISOString().split('T')[0],
69
69
  expires: metadata.expires || '2099-12-31',
70
+ bg_image: metadata.bg_image || null,
71
+ bg_color: metadata.bg_color || null,
72
+ icon: metadata.icon || null,
70
73
  cta: {
71
74
  label: metadata.cta_label || 'Learn More',
72
75
  action: metadata.cta_action || 'link',
@@ -159,7 +162,9 @@ function createPlaygroundServer() {
159
162
  try {
160
163
  // Serve HTML
161
164
  if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
162
- const html = fs.readFileSync(htmlPath, 'utf8');
165
+ const { getVersion } = require('../lib/banner');
166
+ let html = fs.readFileSync(htmlPath, 'utf8');
167
+ html = html.replace('</head>', `<script>window.__VAI_VERSION__="${getVersion()}";</script></head>`);
163
168
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
164
169
  res.end(html);
165
170
  return;
@@ -223,6 +228,43 @@ function createPlaygroundServer() {
223
228
  return;
224
229
  }
225
230
 
231
+ // Serve announcement assets: /assets/announcements/{filename}
232
+ const assetMatch = req.url.match(/^\/assets\/announcements\/([a-zA-Z0-9_.-]+\.(jpg|jpeg|png|webp|gif))$/);
233
+ if (req.method === 'GET' && assetMatch) {
234
+ const assetPath = path.join(__dirname, '..', 'playground', 'assets', 'announcements', assetMatch[1]);
235
+ if (fs.existsSync(assetPath)) {
236
+ const mimeTypes = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', gif: 'image/gif' };
237
+ const data = fs.readFileSync(assetPath);
238
+ res.writeHead(200, {
239
+ 'Content-Type': mimeTypes[assetMatch[2]] || 'application/octet-stream',
240
+ 'Cache-Control': 'public, max-age=86400',
241
+ });
242
+ res.end(data);
243
+ } else {
244
+ res.writeHead(404);
245
+ res.end('Asset not found');
246
+ }
247
+ return;
248
+ }
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
+
226
268
  // API: Models
227
269
  if (req.method === 'GET' && req.url === '/api/models') {
228
270
  const models = MODEL_CATALOG.filter(m => !m.legacy && !m.local && !m.unreleased);
@@ -360,6 +402,14 @@ function createPlaygroundServer() {
360
402
  return;
361
403
  }
362
404
 
405
+ // API: Workflow node help
406
+ if (req.method === 'GET' && req.url === '/api/workflows/node-help') {
407
+ const nodeHelp = require('../playground/help/workflow-nodes');
408
+ res.writeHead(200, { 'Content-Type': 'application/json' });
409
+ res.end(JSON.stringify({ nodeHelp }));
410
+ return;
411
+ }
412
+
363
413
  // API: Chat config (GET)
364
414
  if (req.method === 'GET' && req.url === '/api/chat/config') {
365
415
  const { resolveLLMConfig } = require('../lib/llm');
@@ -428,6 +478,62 @@ function createPlaygroundServer() {
428
478
  return;
429
479
  }
430
480
 
481
+ // API: MCP status — installation status across all tools
482
+ if (req.method === 'GET' && req.url === '/api/mcp/status') {
483
+ try {
484
+ const { statusAll } = require('../mcp/install');
485
+ const results = statusAll();
486
+ res.writeHead(200, { 'Content-Type': 'application/json' });
487
+ res.end(JSON.stringify(results));
488
+ } catch (err) {
489
+ res.writeHead(500, { 'Content-Type': 'application/json' });
490
+ res.end(JSON.stringify({ error: err.message }));
491
+ }
492
+ return;
493
+ }
494
+
495
+ // API: MCP install — install vai into a target tool
496
+ if (req.method === 'POST' && req.url === '/api/mcp/install') {
497
+ try {
498
+ const body = await readBody(req);
499
+ const { target, force } = JSON.parse(body);
500
+ if (!target) {
501
+ res.writeHead(400, { 'Content-Type': 'application/json' });
502
+ res.end(JSON.stringify({ error: 'target is required' }));
503
+ return;
504
+ }
505
+ const { installTarget } = require('../mcp/install');
506
+ const result = installTarget(target, { force: force || false });
507
+ res.writeHead(200, { 'Content-Type': 'application/json' });
508
+ res.end(JSON.stringify(result));
509
+ } catch (err) {
510
+ res.writeHead(500, { 'Content-Type': 'application/json' });
511
+ res.end(JSON.stringify({ error: err.message }));
512
+ }
513
+ return;
514
+ }
515
+
516
+ // API: MCP uninstall — remove vai from a target tool
517
+ if (req.method === 'POST' && req.url === '/api/mcp/uninstall') {
518
+ try {
519
+ const body = await readBody(req);
520
+ const { target } = JSON.parse(body);
521
+ if (!target) {
522
+ res.writeHead(400, { 'Content-Type': 'application/json' });
523
+ res.end(JSON.stringify({ error: 'target is required' }));
524
+ return;
525
+ }
526
+ const { uninstallTarget } = require('../mcp/install');
527
+ const result = uninstallTarget(target);
528
+ res.writeHead(200, { 'Content-Type': 'application/json' });
529
+ res.end(JSON.stringify(result));
530
+ } catch (err) {
531
+ res.writeHead(500, { 'Content-Type': 'application/json' });
532
+ res.end(JSON.stringify({ error: err.message }));
533
+ }
534
+ return;
535
+ }
536
+
431
537
  // API: Settings origins — where each config value comes from
432
538
  if (req.method === 'GET' && req.url === '/api/settings/origins') {
433
539
  const { resolveLLMConfig } = require('../lib/llm');
@@ -656,6 +762,17 @@ function createPlaygroundServer() {
656
762
  description: def.description || '',
657
763
  }));
658
764
 
765
+ // Derive capabilities from tools
766
+ const toolsList = vai.tools || [];
767
+ const capabilities = [];
768
+ if (toolsList.includes('http')) capabilities.push('NETWORK');
769
+ if (toolsList.includes('ingest') || toolsList.includes('aggregate')) capabilities.push('WRITE_DB');
770
+ if (toolsList.includes('generate')) capabilities.push('LLM');
771
+ if (toolsList.includes('loop') || toolsList.includes('forEach')) capabilities.push('LOOP');
772
+ if (toolsList.some(t => ['query','search','collections','aggregate'].includes(t))) capabilities.push('READ_DB');
773
+
774
+ const isOfficial = (r.name || '').startsWith('@vaicli/');
775
+
659
776
  return {
660
777
  name: shortName,
661
778
  packageName: r.name,
@@ -663,11 +780,10 @@ function createPlaygroundServer() {
663
780
  description: r.description || '',
664
781
  category,
665
782
  tags: vai.tags || [],
666
- tools: vai.tools || [],
667
- steps: vai.steps || (vai.tools || []).length || 0,
668
- tools: vai.tools || [],
669
- toolCount: (vai.tools || []).length,
670
- tier: (r.name || '').startsWith('@vaicli/') ? 'official' : 'community',
783
+ tools: toolsList,
784
+ steps: vai.steps || toolsList.length || 0,
785
+ toolCount: toolsList.length,
786
+ tier: isOfficial ? 'official' : 'community',
671
787
  downloads: 0,
672
788
  featured: FEATURED.includes(shortName),
673
789
  installed: installedNames.has(r.name),
@@ -676,6 +792,10 @@ function createPlaygroundServer() {
676
792
  author,
677
793
  assets,
678
794
  inputs,
795
+ capabilities,
796
+ verified: isOfficial,
797
+ security: [],
798
+ rating: null,
679
799
  };
680
800
  });
681
801
 
@@ -1374,15 +1494,23 @@ function createPlaygroundServer() {
1374
1494
  // API: Validate a workflow definition
1375
1495
  if (req.url === '/api/workflows/validate') {
1376
1496
  const { validateWorkflow } = require('../lib/workflow');
1377
- const { definition } = parsed;
1497
+ const { definition, mode } = parsed;
1378
1498
  if (!definition) {
1379
1499
  res.writeHead(400, { 'Content-Type': 'application/json' });
1380
1500
  res.end(JSON.stringify({ error: 'definition is required' }));
1381
1501
  return;
1382
1502
  }
1383
- const errors = validateWorkflow(definition);
1503
+ const validationMode = mode || 'strict';
1504
+ const result = validateWorkflow(definition, { mode: validationMode });
1505
+
1384
1506
  res.writeHead(200, { 'Content-Type': 'application/json' });
1385
- res.end(JSON.stringify({ valid: errors.length === 0, errors }));
1507
+
1508
+ if (validationMode === 'draft') {
1509
+ res.end(JSON.stringify(result));
1510
+ } else {
1511
+ // Backward compatible format for strict mode
1512
+ res.end(JSON.stringify({ valid: result.length === 0, errors: result }));
1513
+ }
1386
1514
  return;
1387
1515
  }
1388
1516
 
@@ -1486,6 +1614,32 @@ function createPlaygroundServer() {
1486
1614
  }
1487
1615
  }
1488
1616
 
1617
+ // ── Export API endpoints ──
1618
+ const exportMatch = req.url.match(/^\/api\/export\/(workflow|chat|search|benchmark)$/);
1619
+ if (req.method === 'POST' && exportMatch) {
1620
+ const context = exportMatch[1];
1621
+ try {
1622
+ const body = JSON.parse(await readBody(req));
1623
+ const { exportArtifact } = require('../lib/export');
1624
+ const result = await exportArtifact({
1625
+ context,
1626
+ format: body.format || 'json',
1627
+ data: body.data || {},
1628
+ options: body.options || {},
1629
+ });
1630
+ const isBinary = Buffer.isBuffer(result.content);
1631
+ res.writeHead(200, {
1632
+ 'Content-Type': result.mimeType,
1633
+ 'Content-Disposition': `attachment; filename="${result.suggestedFilename}"`,
1634
+ });
1635
+ res.end(isBinary ? result.content : result.content);
1636
+ } catch (err) {
1637
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1638
+ res.end(JSON.stringify({ error: err.message }));
1639
+ }
1640
+ return;
1641
+ }
1642
+
1489
1643
  // 404
1490
1644
  res.writeHead(404, { 'Content-Type': 'application/json' });
1491
1645
  res.end(JSON.stringify({ error: 'Not found' }));
@@ -5,6 +5,7 @@ const { generateEmbeddings, apiRequest } = require('../lib/api');
5
5
  const { getMongoCollection } = require('../lib/mongo');
6
6
  const { loadProject } = require('../lib/project');
7
7
  const ui = require('../lib/ui');
8
+ const { showCombinedCostSummary } = require('../lib/cost-display');
8
9
 
9
10
  /**
10
11
  * Register the query command on a Commander program.
@@ -33,6 +34,7 @@ function registerQuery(program) {
33
34
  .option('-q, --quiet', 'Suppress non-essential output')
34
35
  .action(async (text, opts) => {
35
36
  let client;
37
+ const telemetry = require('../lib/telemetry');
36
38
  try {
37
39
  // Merge project config
38
40
  const { config: proj } = loadProject();
@@ -51,6 +53,14 @@ function registerQuery(program) {
51
53
  process.exit(1);
52
54
  }
53
55
 
56
+ const done = telemetry.timer('cli_query', {
57
+ model,
58
+ rerankModel: doRerank ? rerankModel : undefined,
59
+ rerank: doRerank,
60
+ limit: opts.limit,
61
+ topK: opts.topK,
62
+ });
63
+
54
64
  const useColor = !opts.json;
55
65
  const useSpinner = useColor && !opts.quiet;
56
66
 
@@ -242,8 +252,14 @@ function registerQuery(program) {
242
252
  if (!opts.quiet) {
243
253
  const totalTokens = embedTokens + rerankTokens;
244
254
  console.log(ui.dim(` Tokens: ${totalTokens} (embed: ${embedTokens}${rerankTokens ? `, rerank: ${rerankTokens}` : ''})`));
255
+ const costOps = [{ model, tokens: embedTokens, label: `embed (${model})` }];
256
+ if (rerankTokens) costOps.push({ model: rerankModel, tokens: rerankTokens, label: `rerank (${rerankModel})` });
257
+ showCombinedCostSummary(costOps, opts);
245
258
  }
259
+
260
+ done({ resultCount: finalResults.length });
246
261
  } catch (err) {
262
+ telemetry.send('cli_error', { command: 'query', errorType: err.constructor.name });
247
263
  console.error(ui.error(err.message));
248
264
  process.exit(1);
249
265
  } finally {
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const { DEFAULT_RERANK_MODEL } = require('../lib/catalog');
5
5
  const { apiRequest } = require('../lib/api');
6
6
  const ui = require('../lib/ui');
7
+ const { showCostSummary } = require('../lib/cost-display');
7
8
 
8
9
  /**
9
10
  * Register the rerank command on a Commander program.
@@ -25,6 +26,7 @@ function registerRerank(program) {
25
26
  .option('--json', 'Machine-readable JSON output')
26
27
  .option('-q, --quiet', 'Suppress non-essential output')
27
28
  .action(async (opts) => {
29
+ const telemetry = require('../lib/telemetry');
28
30
  try {
29
31
  let documents = opts.documents;
30
32
 
@@ -81,6 +83,12 @@ function registerRerank(program) {
81
83
  opts.model = chosenModel;
82
84
  }
83
85
 
86
+ const done = telemetry.timer('cli_rerank', {
87
+ model: opts.model,
88
+ docCount: documents.length,
89
+ topK: opts.topK,
90
+ });
91
+
84
92
  const body = {
85
93
  query: opts.query,
86
94
  documents,
@@ -120,6 +128,7 @@ function registerRerank(program) {
120
128
  if (result.usage) {
121
129
  console.log(ui.label('Tokens', ui.dim(String(result.usage.total_tokens))));
122
130
  }
131
+ showCostSummary(result.model || opts.model, result.usage?.total_tokens || 0, opts);
123
132
  console.log('');
124
133
  }
125
134
 
@@ -134,7 +143,10 @@ function registerRerank(program) {
134
143
 
135
144
  console.log('');
136
145
  console.log(ui.success('Reranking complete'));
146
+
147
+ done();
137
148
  } catch (err) {
149
+ telemetry.send('cli_error', { command: 'rerank', errorType: err.constructor.name });
138
150
  console.error(ui.error(err.message));
139
151
  process.exit(1);
140
152
  }
@@ -55,7 +55,9 @@ function registerScaffold(program) {
55
55
  .option('--dry-run', 'Show what would be created without writing')
56
56
  .option('-q, --quiet', 'Suppress non-essential output')
57
57
  .action(async (name, opts) => {
58
+ const telemetry = require('../lib/telemetry');
58
59
  try {
60
+ telemetry.send('cli_scaffold', { template: opts.target });
59
61
  const target = opts.target;
60
62
  const projectDir = path.resolve(process.cwd(), name);
61
63
 
@@ -4,6 +4,7 @@ const { getDefaultModel } = require('../lib/catalog');
4
4
  const { generateEmbeddings } = require('../lib/api');
5
5
  const { getMongoCollection } = require('../lib/mongo');
6
6
  const ui = require('../lib/ui');
7
+ const { showCostSummary } = require('../lib/cost-display');
7
8
 
8
9
  /**
9
10
  * Register the search command on a Commander program.
@@ -29,7 +30,13 @@ function registerSearch(program) {
29
30
  .option('-q, --quiet', 'Suppress non-essential output')
30
31
  .action(async (opts) => {
31
32
  let client;
33
+ const telemetry = require('../lib/telemetry');
32
34
  try {
35
+ const done = telemetry.timer('cli_search', {
36
+ model: opts.model,
37
+ limit: opts.limit,
38
+ });
39
+
33
40
  const useColor = !opts.json;
34
41
  const useSpinner = useColor && !opts.quiet;
35
42
  let spin;
@@ -93,9 +100,12 @@ function registerSearch(program) {
93
100
  if (!opts.quiet) {
94
101
  console.log(ui.label('Query', ui.cyan(`"${opts.query}"`)));
95
102
  console.log(ui.label('Results', String(cleanResults.length)));
103
+ showCostSummary(opts.model, embedResult.usage?.total_tokens || 0, opts);
96
104
  console.log('');
97
105
  }
98
106
 
107
+ done({ resultCount: cleanResults.length });
108
+
99
109
  if (cleanResults.length === 0) {
100
110
  console.log(ui.yellow('No results found.'));
101
111
  return;
@@ -113,6 +123,7 @@ function registerSearch(program) {
113
123
  console.log('');
114
124
  }
115
125
  } catch (err) {
126
+ telemetry.send('cli_error', { command: 'search', errorType: err.constructor.name });
116
127
  console.error(ui.error(err.message));
117
128
  process.exit(1);
118
129
  } finally {
@@ -5,6 +5,7 @@ const { generateEmbeddings } = require('../lib/api');
5
5
  const { cosineSimilarity } = require('../lib/math');
6
6
  const { getDefaultModel } = require('../lib/catalog');
7
7
  const ui = require('../lib/ui');
8
+ const { showCostSummary } = require('../lib/cost-display');
8
9
 
9
10
  /**
10
11
  * Register the similarity command on a Commander program.
@@ -23,6 +24,7 @@ function registerSimilarity(program) {
23
24
  .option('--json', 'Machine-readable JSON output')
24
25
  .option('-q, --quiet', 'Suppress non-essential output')
25
26
  .action(async (texts, opts) => {
27
+ const telemetry = require('../lib/telemetry');
26
28
  try {
27
29
  let textA = null;
28
30
  let compareTexts = [];
@@ -57,6 +59,8 @@ function registerSimilarity(program) {
57
59
  process.exit(1);
58
60
  }
59
61
 
62
+ const done = telemetry.timer('cli_similarity', { model: opts.model });
63
+
60
64
  // Batch all texts into one API call
61
65
  const allTexts = [textA, ...compareTexts];
62
66
 
@@ -113,6 +117,7 @@ function registerSimilarity(program) {
113
117
  console.log(ui.label('Text B', `"${truncate(compareTexts[0], 70)}"`));
114
118
  console.log(ui.label('Model', ui.cyan(model)));
115
119
  console.log(ui.label('Tokens', ui.dim(String(tokens))));
120
+ showCostSummary(model, tokens, opts);
116
121
  console.log('');
117
122
  } else {
118
123
  // One-vs-many comparison
@@ -152,9 +157,13 @@ function registerSimilarity(program) {
152
157
 
153
158
  console.log('');
154
159
  console.log(` ${ui.dim(`${results.length} comparisons, ${tokens} tokens`)}`);
160
+ showCostSummary(model, tokens, opts);
155
161
  console.log('');
156
162
  }
163
+
164
+ done();
157
165
  } catch (err) {
166
+ telemetry.send('cli_error', { command: 'similarity', errorType: err.constructor.name });
158
167
  console.error(ui.error(err.message));
159
168
  process.exit(1);
160
169
  }
@@ -29,6 +29,8 @@ function registerStore(program) {
29
29
  .option('-q, --quiet', 'Suppress non-essential output')
30
30
  .action(async (opts) => {
31
31
  let client;
32
+ const telemetry = require('../lib/telemetry');
33
+ const done = telemetry.timer('cli_store', { model: opts.model });
32
34
  try {
33
35
  // Batch mode: .jsonl file
34
36
  if (opts.file && opts.file.endsWith('.jsonl')) {
@@ -102,7 +104,9 @@ function registerStore(program) {
102
104
  console.log(ui.label('Tokens', String(embedResult.usage.total_tokens)));
103
105
  }
104
106
  }
107
+ done();
105
108
  } catch (err) {
109
+ telemetry.send('cli_error', { command: 'store', errorType: err.constructor.name });
106
110
  console.error(ui.error(err.message));
107
111
  process.exit(1);
108
112
  } finally {