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.
- package/README.md +82 -8
- package/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/commands/benchmark.js +22 -8
- package/src/commands/chat.js +50 -11
- package/src/commands/chunk.js +10 -0
- package/src/commands/demo.js +4 -0
- package/src/commands/embed.js +13 -0
- package/src/commands/estimate.js +3 -0
- package/src/commands/eval.js +6 -0
- package/src/commands/explain.js +2 -0
- package/src/commands/export.js +124 -0
- package/src/commands/generate.js +2 -0
- package/src/commands/import.js +195 -0
- package/src/commands/index-workspace.js +239 -0
- package/src/commands/ingest.js +4 -0
- package/src/commands/init.js +2 -0
- package/src/commands/mcp-server.js +115 -3
- package/src/commands/models.js +2 -0
- package/src/commands/ping.js +7 -0
- package/src/commands/pipeline.js +15 -0
- package/src/commands/playground.js +163 -9
- package/src/commands/query.js +16 -0
- package/src/commands/rerank.js +12 -0
- package/src/commands/scaffold.js +2 -0
- package/src/commands/search.js +11 -0
- package/src/commands/similarity.js +9 -0
- package/src/commands/store.js +4 -0
- package/src/commands/workflow.js +286 -0
- package/src/lib/capability-report.js +134 -0
- package/src/lib/chat.js +32 -1
- package/src/lib/config.js +2 -0
- package/src/lib/cost-display.js +107 -0
- package/src/lib/explanations.js +6 -0
- package/src/lib/export/contexts/benchmark-export.js +27 -0
- package/src/lib/export/contexts/chat-export.js +41 -0
- package/src/lib/export/contexts/explore-export.js +22 -0
- package/src/lib/export/contexts/search-export.js +54 -0
- package/src/lib/export/contexts/workflow-export.js +80 -0
- package/src/lib/export/formats/clipboard-export.js +29 -0
- package/src/lib/export/formats/csv-export.js +45 -0
- package/src/lib/export/formats/json-export.js +50 -0
- package/src/lib/export/formats/markdown-export.js +189 -0
- package/src/lib/export/formats/mermaid-export.js +274 -0
- package/src/lib/export/formats/pdf-export.js +117 -0
- package/src/lib/export/formats/png-export.js +96 -0
- package/src/lib/export/formats/svg-export.js +116 -0
- package/src/lib/export/index.js +175 -0
- package/src/lib/llm.js +125 -18
- package/src/lib/quality-audit.js +71 -0
- package/src/lib/security/blocked-domains.json +17 -0
- package/src/lib/security-audit.js +198 -0
- package/src/lib/telemetry.js +23 -1
- package/src/lib/workflow-scaffold.js +61 -0
- package/src/lib/workflow-test-runner.js +208 -0
- package/src/lib/workflow.js +333 -28
- package/src/mcp/install.js +280 -7
- package/src/mcp/schemas/index.js +40 -0
- package/src/mcp/server.js +2 -0
- package/src/mcp/tools/workspace.js +463 -0
- package/src/playground/announcements.md +56 -0
- package/src/playground/help/workflow-nodes.js +472 -0
- package/src/playground/index.html +13134 -8507
- package/src/playground/vendor/mermaid.min.js +2811 -0
- package/src/workflows/rag-chat.json +165 -0
- package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
- package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
- package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
- package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
- package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
- package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
- package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
- package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
- package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
- package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
- package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
- package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
- package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
- package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
- package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
- package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
- package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
- package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
- package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
- 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 };
|
package/src/commands/ingest.js
CHANGED
|
@@ -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 {
|
package/src/commands/init.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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 };
|
package/src/commands/models.js
CHANGED
|
@@ -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
|
package/src/commands/ping.js
CHANGED
|
@@ -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));
|
package/src/commands/pipeline.js
CHANGED
|
@@ -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 {
|