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
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const ui = require('../lib/ui');
6
+ const { validateWorkflow } = require('../lib/workflow');
7
+
8
+ /**
9
+ * Register the import command on a Commander program.
10
+ * @param {import('commander').Command} program
11
+ */
12
+ function registerImport(program) {
13
+ program
14
+ .command('import <file>')
15
+ .description('Import a workflow definition or chat session exported by vai')
16
+ .option('--type <type>', 'Force type: workflow or chat (auto-detected if omitted)')
17
+ .option('--db <name>', 'Target database (for chat import)')
18
+ .option('--collection <name>', 'Target collection (for chat import)')
19
+ .option('--dry-run', 'Show what would be imported without writing')
20
+ .option('--json', 'Machine-readable JSON output')
21
+ .action(async (file, opts) => {
22
+ try {
23
+ const resolved = path.resolve(file);
24
+ if (!fs.existsSync(resolved)) {
25
+ throw new Error(`File not found: ${resolved}`);
26
+ }
27
+
28
+ const raw = fs.readFileSync(resolved, 'utf-8');
29
+ const data = JSON.parse(raw);
30
+
31
+ // Auto-detect type
32
+ const type = opts.type || detectType(data);
33
+ if (!type) {
34
+ throw new Error(
35
+ 'Could not auto-detect import type. Use --type workflow or --type chat.'
36
+ );
37
+ }
38
+
39
+ if (type === 'workflow') {
40
+ await importWorkflow(data, opts, resolved);
41
+ } else if (type === 'chat') {
42
+ await importChat(data, opts, resolved);
43
+ } else {
44
+ throw new Error(`Unknown import type: "${type}". Use "workflow" or "chat".`);
45
+ }
46
+ } catch (err) {
47
+ if (opts.json) {
48
+ console.log(JSON.stringify({ error: err.message }));
49
+ } else {
50
+ console.error(ui.error ? ui.error(err.message) : `✗ ${err.message}`);
51
+ }
52
+ process.exitCode = 1;
53
+ }
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Detect whether the JSON is a workflow or chat session.
59
+ */
60
+ function detectType(data) {
61
+ // Workflow: has steps array
62
+ if (Array.isArray(data.steps) && data.steps.length > 0) return 'workflow';
63
+ // Chat: has turns or session object with turns
64
+ if (data.turns || (data.session && data.session.turns)) return 'chat';
65
+ // Exported chat wrapper
66
+ if (data._context === 'chat') return 'chat';
67
+ if (data._context === 'workflow') return 'workflow';
68
+ // Workflow with name + steps
69
+ if (data.name && data.steps) return 'workflow';
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * Import a workflow definition.
75
+ */
76
+ async function importWorkflow(data, opts, filePath) {
77
+ // Strip export metadata
78
+ const workflow = { ...data };
79
+ delete workflow._exportMeta;
80
+ delete workflow._metadata;
81
+ delete workflow._execution;
82
+ delete workflow._context;
83
+ delete workflow._dependencyMap;
84
+ delete workflow._executionLayers;
85
+
86
+ // Validate
87
+ const errors = validateWorkflow(workflow);
88
+ if (errors.length > 0) {
89
+ throw new Error(`Invalid workflow:\n ${errors.join('\n ')}`);
90
+ }
91
+
92
+ const stepCount = (workflow.steps || []).length;
93
+ const inputCount = Object.keys(workflow.inputs || {}).length;
94
+
95
+ if (opts.dryRun) {
96
+ const summary = {
97
+ type: 'workflow',
98
+ name: workflow.name,
99
+ version: workflow.version,
100
+ steps: stepCount,
101
+ inputs: inputCount,
102
+ valid: true,
103
+ };
104
+ if (opts.json) {
105
+ console.log(JSON.stringify({ dryRun: true, ...summary }));
106
+ } else {
107
+ console.log(ui.success ? ui.success('Dry run — workflow is valid') : '✓ Dry run — workflow is valid');
108
+ console.log(` Name: ${workflow.name}`);
109
+ console.log(` Version: ${workflow.version || '—'}`);
110
+ console.log(` Steps: ${stepCount}`);
111
+ console.log(` Inputs: ${inputCount}`);
112
+ }
113
+ return;
114
+ }
115
+
116
+ // Write cleaned workflow to a local file
117
+ const outName = `${workflow.name || 'imported-workflow'}.json`;
118
+ const outPath = path.resolve(outName);
119
+ fs.writeFileSync(outPath, JSON.stringify(workflow, null, 2) + '\n', 'utf-8');
120
+
121
+ if (opts.json) {
122
+ console.log(JSON.stringify({ imported: true, type: 'workflow', path: outPath, steps: stepCount }));
123
+ } else {
124
+ console.log(ui.success ? ui.success(`Imported workflow → ${outPath}`) : `✓ Imported workflow → ${outPath}`);
125
+ console.log(` Name: ${workflow.name} | Steps: ${stepCount} | Inputs: ${inputCount}`);
126
+ console.log(` Run with: vai workflow run ${outName}`);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Import a chat session into MongoDB.
132
+ */
133
+ async function importChat(data, opts, filePath) {
134
+ // Unwrap export format
135
+ const session = data.session || data;
136
+ const turns = session.turns || [];
137
+
138
+ if (turns.length === 0) {
139
+ throw new Error('Chat session has no turns to import.');
140
+ }
141
+
142
+ const summary = {
143
+ type: 'chat',
144
+ sessionId: session.sessionId || session.id,
145
+ turns: turns.length,
146
+ provider: session.provider,
147
+ model: session.model,
148
+ };
149
+
150
+ if (opts.dryRun) {
151
+ if (opts.json) {
152
+ console.log(JSON.stringify({ dryRun: true, ...summary }));
153
+ } else {
154
+ console.log(ui.success ? ui.success('Dry run — chat session is valid') : '✓ Dry run — chat session is valid');
155
+ console.log(` Session: ${summary.sessionId || '—'}`);
156
+ console.log(` Turns: ${summary.turns}`);
157
+ console.log(` Provider: ${summary.provider || '—'} (${summary.model || '—'})`);
158
+ }
159
+ return;
160
+ }
161
+
162
+ // Import to MongoDB
163
+ const dbName = opts.db || 'vai';
164
+ const collName = opts.collection || 'chat_history';
165
+
166
+ const { getMongoCollection } = require('../lib/mongo');
167
+ const { client, collection } = await getMongoCollection(dbName, collName);
168
+
169
+ try {
170
+ const docs = turns.map((turn, i) => ({
171
+ sessionId: session.sessionId || session.id || `imported-${Date.now()}`,
172
+ role: turn.role,
173
+ content: turn.content,
174
+ timestamp: turn.timestamp ? new Date(turn.timestamp) : new Date(),
175
+ ...(turn.context ? { context: turn.context } : {}),
176
+ ...(turn.metadata ? { metadata: turn.metadata } : {}),
177
+ _imported: true,
178
+ _importedAt: new Date(),
179
+ _importSource: path.basename(filePath),
180
+ }));
181
+
182
+ const result = await collection.insertMany(docs);
183
+
184
+ if (opts.json) {
185
+ console.log(JSON.stringify({ imported: true, ...summary, inserted: result.insertedCount, db: dbName, collection: collName }));
186
+ } else {
187
+ console.log(ui.success ? ui.success(`Imported ${result.insertedCount} turns → ${dbName}.${collName}`) : `✓ Imported ${result.insertedCount} turns → ${dbName}.${collName}`);
188
+ console.log(` Session: ${summary.sessionId || '—'} | Provider: ${summary.provider || '—'}`);
189
+ }
190
+ } finally {
191
+ await client.close();
192
+ }
193
+ }
194
+
195
+ module.exports = { registerImport, detectType };
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const ora = require('ora');
5
+ const pc = require('picocolors');
6
+
7
+ /**
8
+ * Register the index-workspace command.
9
+ * @param {import('commander').Command} program
10
+ */
11
+ function registerIndexWorkspace(program) {
12
+ program
13
+ .command('index-workspace [path]')
14
+ .alias('index-ws')
15
+ .description('Index a workspace/codebase for semantic code search')
16
+ .option('--db <name>', 'MongoDB database name')
17
+ .option('--collection <name>', 'Collection to store indexed documents')
18
+ .option('--content-type <type>', 'Content type: code, docs, config, or all', 'code')
19
+ .option('--model <name>', 'Embedding model', 'voyage-code-3')
20
+ .option('--max-files <n>', 'Maximum files to index', (v) => parseInt(v, 10), 1000)
21
+ .option('--max-file-size <bytes>', 'Maximum file size in bytes', (v) => parseInt(v, 10), 100000)
22
+ .option('--chunk-size <n>', 'Target chunk size in characters', (v) => parseInt(v, 10), 512)
23
+ .option('--chunk-overlap <n>', 'Overlap between chunks', (v) => parseInt(v, 10), 50)
24
+ .option('--batch-size <n>', 'Files per batch', (v) => parseInt(v, 10), 10)
25
+ .option('--create-index', 'Create vector search index after indexing')
26
+ .option('--json', 'Output as JSON')
27
+ .action(async (workspacePath, opts) => {
28
+ const telemetry = require('../lib/telemetry');
29
+ telemetry.send('cli_index_workspace_run', { contentType: opts.contentType });
30
+
31
+ const { handleIndexWorkspace } = require('../mcp/tools/workspace');
32
+ const resolvedPath = workspacePath ? path.resolve(workspacePath) : process.cwd();
33
+
34
+ const spinner = ora(`Indexing ${resolvedPath}...`).start();
35
+
36
+ try {
37
+ const result = await handleIndexWorkspace({
38
+ path: resolvedPath,
39
+ db: opts.db,
40
+ collection: opts.collection,
41
+ contentType: opts.contentType,
42
+ model: opts.model,
43
+ maxFiles: opts.maxFiles,
44
+ maxFileSize: opts.maxFileSize,
45
+ chunkSize: opts.chunkSize,
46
+ chunkOverlap: opts.chunkOverlap,
47
+ batchSize: opts.batchSize,
48
+ });
49
+
50
+ spinner.stop();
51
+
52
+ if (opts.json) {
53
+ console.log(JSON.stringify(result.structuredContent, null, 2));
54
+ } else {
55
+ const stats = result.structuredContent;
56
+ console.log('\n' + pc.green('Workspace indexed successfully!') + '\n');
57
+ console.log(` Files found: ${stats.filesFound}`);
58
+ console.log(` Files indexed: ${stats.filesIndexed}`);
59
+ console.log(` Chunks created: ${stats.chunksCreated}`);
60
+ console.log(` Time: ${stats.timeMs}ms`);
61
+ console.log(` Collection: ${stats.db}.${stats.collection}`);
62
+ console.log(` Model: ${stats.model}`);
63
+
64
+ if (stats.errors?.length > 0) {
65
+ console.log('\n' + pc.yellow(`Errors (${stats.errors.length}):`));
66
+ for (const err of stats.errors.slice(0, 5)) {
67
+ console.log(` ${pc.dim(err.file)}: ${err.error}`);
68
+ }
69
+ if (stats.errors.length > 5) {
70
+ console.log(` ... and ${stats.errors.length - 5} more`);
71
+ }
72
+ }
73
+ }
74
+
75
+ // Create index if requested
76
+ if (opts.createIndex) {
77
+ const indexSpinner = ora('Creating vector search index...').start();
78
+ try {
79
+ const { createVectorIndex } = require('../lib/mongo');
80
+ await createVectorIndex(
81
+ result.structuredContent.db,
82
+ result.structuredContent.collection,
83
+ 'vector_index',
84
+ 'embedding'
85
+ );
86
+ indexSpinner.succeed('Vector search index created');
87
+ } catch (err) {
88
+ indexSpinner.fail(`Failed to create index: ${err.message}`);
89
+ }
90
+ }
91
+
92
+ } catch (err) {
93
+ spinner.fail(`Indexing failed: ${err.message}`);
94
+ process.exit(1);
95
+ }
96
+ });
97
+
98
+ // Search code command
99
+ program
100
+ .command('search-code <query>')
101
+ .alias('sc')
102
+ .description('Semantic code search across indexed codebase')
103
+ .option('--db <name>', 'MongoDB database name')
104
+ .option('--collection <name>', 'Collection with indexed code')
105
+ .option('--limit <n>', 'Maximum results', (v) => parseInt(v, 10), 10)
106
+ .option('--language <lang>', 'Filter by programming language (e.g., js, py, go)')
107
+ .option('--category <cat>', 'Filter by category: code, docs, config')
108
+ .option('--model <name>', 'Embedding model')
109
+ .option('--json', 'Output as JSON')
110
+ .action(async (query, opts) => {
111
+ const telemetry = require('../lib/telemetry');
112
+ telemetry.send('cli_search_code_run', { language: opts.language });
113
+
114
+ const { handleSearchCode } = require('../mcp/tools/workspace');
115
+ const spinner = ora('Searching...').start();
116
+
117
+ try {
118
+ const result = await handleSearchCode({
119
+ query,
120
+ db: opts.db,
121
+ collection: opts.collection,
122
+ limit: opts.limit,
123
+ language: opts.language,
124
+ category: opts.category,
125
+ model: opts.model,
126
+ });
127
+
128
+ spinner.stop();
129
+
130
+ if (opts.json) {
131
+ console.log(JSON.stringify(result.structuredContent, null, 2));
132
+ } else {
133
+ const results = result.structuredContent.results;
134
+ const meta = result.structuredContent.metadata;
135
+
136
+ console.log(`\n${pc.bold(`Found ${results.length} results`)} ${pc.dim(`(${meta.timeMs}ms)`)}\n`);
137
+
138
+ for (let i = 0; i < results.length; i++) {
139
+ const r = results[i];
140
+ const score = (r.score * 100).toFixed(1);
141
+
142
+ console.log(pc.cyan(`[${i + 1}] ${r.source}`) + pc.dim(` (${r.language || 'unknown'}) — ${score}%`));
143
+
144
+ if (r.symbols?.length > 0) {
145
+ console.log(pc.dim(` Symbols: ${r.symbols.slice(0, 5).join(', ')}`));
146
+ }
147
+
148
+ // Show snippet
149
+ const snippet = r.content.slice(0, 200).replace(/\n/g, '\n ');
150
+ console.log(pc.dim(' ' + snippet + (r.content.length > 200 ? '...' : '')));
151
+ console.log('');
152
+ }
153
+ }
154
+
155
+ } catch (err) {
156
+ spinner.fail(`Search failed: ${err.message}`);
157
+ process.exit(1);
158
+ }
159
+ });
160
+
161
+ // Explain code command (context retrieval for code)
162
+ program
163
+ .command('context-code')
164
+ .alias('ctx')
165
+ .description('Get contextual information for code from indexed documentation')
166
+ .option('--code <snippet>', 'Code snippet to explain')
167
+ .option('--file <path>', 'File containing code to explain')
168
+ .option('--language <lang>', 'Programming language')
169
+ .option('--db <name>', 'MongoDB database name')
170
+ .option('--collection <name>', 'Collection with indexed documentation')
171
+ .option('--context-limit <n>', 'Number of context documents', (v) => parseInt(v, 10), 5)
172
+ .option('--json', 'Output as JSON')
173
+ .action(async (opts) => {
174
+ const telemetry = require('../lib/telemetry');
175
+ telemetry.send('cli_explain_code_run');
176
+
177
+ const { handleExplainCode } = require('../mcp/tools/workspace');
178
+ const fs = require('fs');
179
+
180
+ let code = opts.code;
181
+
182
+ // Read from file if provided
183
+ if (opts.file && !code) {
184
+ try {
185
+ code = fs.readFileSync(opts.file, 'utf-8');
186
+ if (!opts.language) {
187
+ opts.language = path.extname(opts.file).slice(1);
188
+ }
189
+ } catch (err) {
190
+ console.error(`Failed to read file: ${err.message}`);
191
+ process.exit(1);
192
+ }
193
+ }
194
+
195
+ // Read from stdin if no code provided
196
+ if (!code) {
197
+ console.error('Provide code via --code, --file, or pipe to stdin');
198
+ process.exit(1);
199
+ }
200
+
201
+ const spinner = ora('Finding relevant context...').start();
202
+
203
+ try {
204
+ const result = await handleExplainCode({
205
+ code,
206
+ language: opts.language,
207
+ db: opts.db,
208
+ collection: opts.collection,
209
+ contextLimit: opts.contextLimit,
210
+ });
211
+
212
+ spinner.stop();
213
+
214
+ if (opts.json) {
215
+ console.log(JSON.stringify(result.structuredContent, null, 2));
216
+ } else {
217
+ console.log('\n' + pc.bold('Code Context') + '\n');
218
+ console.log(pc.dim('─'.repeat(60)) + '\n');
219
+
220
+ const context = result.structuredContent.context || [];
221
+ if (context.length === 0) {
222
+ console.log(pc.yellow('No relevant context found. Try indexing more documentation.'));
223
+ } else {
224
+ for (const ctx of context) {
225
+ console.log(pc.cyan(`[${ctx.source}]`) + pc.dim(` — ${(ctx.score * 100).toFixed(1)}%`));
226
+ console.log(ctx.content.slice(0, 500));
227
+ console.log(pc.dim('─'.repeat(40)) + '\n');
228
+ }
229
+ }
230
+ }
231
+
232
+ } catch (err) {
233
+ spinner.fail(`Explain failed: ${err.message}`);
234
+ process.exit(1);
235
+ }
236
+ });
237
+ }
238
+
239
+ module.exports = { registerIndexWorkspace };
@@ -208,6 +208,8 @@ function registerIngest(program) {
208
208
  .option('--json', 'Machine-readable JSON output')
209
209
  .option('-q, --quiet', 'Suppress progress, show only final summary')
210
210
  .action(async (opts) => {
211
+ const telemetry = require('../lib/telemetry');
212
+ const done = telemetry.timer('cli_ingest');
211
213
  const startTime = Date.now();
212
214
 
213
215
  // Validate file exists
@@ -394,7 +396,9 @@ function registerIngest(program) {
394
396
  }
395
397
  }
396
398
  }
399
+ done({ format, docCount: succeeded });
397
400
  } catch (err) {
401
+ telemetry.send('cli_error', { command: 'ingest', errorType: err.constructor.name });
398
402
  console.error(ui.error(err.message));
399
403
  process.exit(1);
400
404
  } finally {
@@ -21,6 +21,8 @@ function registerInit(program) {
21
21
  .option('--json', 'Output created config as JSON (non-interactive)')
22
22
  .option('-q, --quiet', 'Suppress non-essential output')
23
23
  .action(async (opts) => {
24
+ const telemetry = require('../lib/telemetry');
25
+ telemetry.send('cli_init');
24
26
  // Check for existing config
25
27
  const existing = findProjectFile();
26
28
  if (existing && !opts.force) {
@@ -17,6 +17,8 @@ function registerMcpServer(program) {
17
17
  .option('--no-sse', 'Disable SSE transport (SSE is enabled by default for HTTP)')
18
18
  .option('--verbose', 'Enable debug logging to stderr')
19
19
  .action(async (opts) => {
20
+ const telemetry = require('../lib/telemetry');
21
+ telemetry.send('cli_mcp_start', { transport: opts.transport });
20
22
  if (opts.verbose) {
21
23
  process.env.VAI_MCP_VERBOSE = '1';
22
24
  }
@@ -51,21 +53,35 @@ function registerMcpServer(program) {
51
53
  // Subcommand: install
52
54
  cmd
53
55
  .command('install [targets...]')
54
- .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")')
55
57
  .option('--force', 'Overwrite existing vai entry', false)
56
58
  .option('--transport <mode>', 'Transport mode: stdio or http', 'stdio')
57
59
  .option('--port <number>', 'HTTP port (http transport only)', (v) => parseInt(v, 10))
58
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)
59
67
  .action((targets, opts) => {
60
68
  const { TARGETS, installTarget } = require('../mcp/install');
61
69
 
62
70
  if (!targets.length) {
63
71
  console.log('Usage: vai mcp install <target|all>');
64
- 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`);
65
78
  return;
66
79
  }
67
80
 
68
- 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;
69
85
 
70
86
  for (const key of keys) {
71
87
  try {
@@ -74,8 +90,22 @@ function registerMcpServer(program) {
74
90
  transport: opts.transport,
75
91
  port: opts.port,
76
92
  apiKey: opts.apiKey,
93
+ mongodbUri: opts.mongodbUri,
94
+ db: opts.db,
95
+ collection: opts.collection,
96
+ workspacePath: opts.workspacePath,
97
+ verbose: opts.verbose,
77
98
  });
78
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
+ }
79
109
  } catch (err) {
80
110
  console.error(`❌ ${key}: ${err.message}`);
81
111
  }
@@ -121,6 +151,88 @@ function registerMcpServer(program) {
121
151
  }
122
152
  console.log('');
123
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
+ });
124
236
  }
125
237
 
126
238
  module.exports = { registerMcpServer };
@@ -46,6 +46,8 @@ function registerModels(program) {
46
46
  .option('--json', 'Machine-readable JSON output')
47
47
  .option('-q, --quiet', 'Suppress non-essential output')
48
48
  .action((opts) => {
49
+ const telemetry = require('../lib/telemetry');
50
+ telemetry.send('cli_models', { category: opts.type });
49
51
  let models = MODEL_CATALOG;
50
52
 
51
53
  // Separate current and legacy models
@@ -15,6 +15,7 @@ function registerPing(program) {
15
15
  .option('-q, --quiet', 'Suppress non-essential output')
16
16
  .option('--mask', 'Mask sensitive info (cluster hostnames, endpoints) in output. Also enabled by VAI_MASK=1 env var.')
17
17
  .action(async (opts) => {
18
+ const telemetry = require('../lib/telemetry');
18
19
  // Support env var so all recordings are masked without remembering the flag
19
20
  if (process.env.VAI_MASK === '1' || process.env.VAI_MASK === 'true') {
20
21
  opts.mask = true;
@@ -242,6 +243,12 @@ function registerPing(program) {
242
243
  }
243
244
  }
244
245
 
246
+ // Telemetry
247
+ telemetry.send('cli_ping', {
248
+ voyageOk: !!results.voyage?.ok,
249
+ mongoOk: !!results.mongodb?.ok,
250
+ });
251
+
245
252
  // Emit JSON at the end with all results
246
253
  if (opts.json) {
247
254
  console.log(JSON.stringify({ ok: true, ...results }, null, 2));
@@ -67,6 +67,7 @@ function registerPipeline(program) {
67
67
  .option('-q, --quiet', 'Suppress non-essential output')
68
68
  .action(async (input, opts) => {
69
69
  let client;
70
+ const telemetry = require('../lib/telemetry');
70
71
  try {
71
72
  // Merge project config
72
73
  const { config: proj } = loadProject();
@@ -89,6 +90,13 @@ function registerPipeline(program) {
89
90
  process.exit(1);
90
91
  }
91
92
 
93
+ const done = telemetry.timer('cli_pipeline', {
94
+ model,
95
+ chunkStrategy: strategy,
96
+ chunkSize,
97
+ createIndex: !!opts.createIndex,
98
+ });
99
+
92
100
  if (!STRATEGIES.includes(strategy)) {
93
101
  console.error(ui.error(`Unknown strategy: "${strategy}". Available: ${STRATEGIES.join(', ')}`));
94
102
  process.exit(1);
@@ -313,7 +321,14 @@ function registerPipeline(program) {
313
321
  console.log('');
314
322
  console.log(ui.dim(' Next: vai query "your search" --db ' + db + ' --collection ' + collection));
315
323
  }
324
+
325
+ done({
326
+ fileCount: files.length,
327
+ chunkCount: allChunks.length,
328
+ docCount: insertResult.insertedCount,
329
+ });
316
330
  } catch (err) {
331
+ telemetry.send('cli_error', { command: 'pipeline', errorType: err.constructor.name });
317
332
  console.error(ui.error(err.message));
318
333
  process.exit(1);
319
334
  } finally {