voyageai-cli
Version:
CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search
244 lines (210 loc) • 9.03 kB
JavaScript
;
const path = require('path');
let ora;
async function getOra() {
if (!ora) { ora = (await import('ora')).default; }
return ora;
}
const pc = require('picocolors');
/**
* Register the index-workspace command.
* @param {import('commander').Command} program
*/
function registerIndexWorkspace(program) {
program
.command('index-workspace [path]')
.alias('index-ws')
.description('Index a workspace/codebase for semantic code search')
.option('--db <name>', 'MongoDB database name')
.option('--collection <name>', 'Collection to store indexed documents')
.option('--content-type <type>', 'Content type: code, docs, config, or all', 'code')
.option('--model <name>', 'Embedding model', 'voyage-code-3')
.option('--max-files <n>', 'Maximum files to index', (v) => parseInt(v, 10), 1000)
.option('--max-file-size <bytes>', 'Maximum file size in bytes', (v) => parseInt(v, 10), 100000)
.option('--chunk-size <n>', 'Target chunk size in characters', (v) => parseInt(v, 10), 512)
.option('--chunk-overlap <n>', 'Overlap between chunks', (v) => parseInt(v, 10), 50)
.option('--batch-size <n>', 'Files per batch', (v) => parseInt(v, 10), 10)
.option('--create-index', 'Create vector search index after indexing')
.option('--json', 'Output as JSON')
.action(async (workspacePath, opts) => {
const telemetry = require('../lib/telemetry');
telemetry.send('cli_index_workspace_run', { contentType: opts.contentType });
const { handleIndexWorkspace } = require('../mcp/tools/workspace');
const resolvedPath = workspacePath ? path.resolve(workspacePath) : process.cwd();
const spinner = (await getOra())(`Indexing ${resolvedPath}...`).start();
try {
const result = await handleIndexWorkspace({
path: resolvedPath,
db: opts.db,
collection: opts.collection,
contentType: opts.contentType,
model: opts.model,
maxFiles: opts.maxFiles,
maxFileSize: opts.maxFileSize,
chunkSize: opts.chunkSize,
chunkOverlap: opts.chunkOverlap,
batchSize: opts.batchSize,
});
spinner.stop();
if (opts.json) {
console.log(JSON.stringify(result.structuredContent, null, 2));
} else {
const stats = result.structuredContent;
console.log('\n' + pc.green('Workspace indexed successfully!') + '\n');
console.log(` Files found: ${stats.filesFound}`);
console.log(` Files indexed: ${stats.filesIndexed}`);
console.log(` Chunks created: ${stats.chunksCreated}`);
console.log(` Time: ${stats.timeMs}ms`);
console.log(` Collection: ${stats.db}.${stats.collection}`);
console.log(` Model: ${stats.model}`);
if (stats.errors?.length > 0) {
console.log('\n' + pc.yellow(`Errors (${stats.errors.length}):`));
for (const err of stats.errors.slice(0, 5)) {
console.log(` ${pc.dim(err.file)}: ${err.error}`);
}
if (stats.errors.length > 5) {
console.log(` ... and ${stats.errors.length - 5} more`);
}
}
}
// Create index if requested
if (opts.createIndex) {
const indexSpinner = (await getOra())('Creating vector search index...').start();
try {
const { createVectorIndex } = require('../lib/mongo');
await createVectorIndex(
result.structuredContent.db,
result.structuredContent.collection,
'vector_index',
'embedding'
);
indexSpinner.succeed('Vector search index created');
} catch (err) {
indexSpinner.fail(`Failed to create index: ${err.message}`);
}
}
} catch (err) {
spinner.fail(`Indexing failed: ${err.message}`);
process.exit(1);
}
});
// Search code command
program
.command('search-code <query>')
.alias('sc')
.description('Semantic code search across indexed codebase')
.option('--db <name>', 'MongoDB database name')
.option('--collection <name>', 'Collection with indexed code')
.option('--limit <n>', 'Maximum results', (v) => parseInt(v, 10), 10)
.option('--language <lang>', 'Filter by programming language (e.g., js, py, go)')
.option('--category <cat>', 'Filter by category: code, docs, config')
.option('--model <name>', 'Embedding model')
.option('--json', 'Output as JSON')
.action(async (query, opts) => {
const telemetry = require('../lib/telemetry');
telemetry.send('cli_search_code_run', { language: opts.language });
const { handleSearchCode } = require('../mcp/tools/workspace');
const spinner = (await getOra())('Searching...').start();
try {
const result = await handleSearchCode({
query,
db: opts.db,
collection: opts.collection,
limit: opts.limit,
language: opts.language,
category: opts.category,
model: opts.model,
});
spinner.stop();
if (opts.json) {
console.log(JSON.stringify(result.structuredContent, null, 2));
} else {
const results = result.structuredContent.results;
const meta = result.structuredContent.metadata;
console.log(`\n${pc.bold(`Found ${results.length} results`)} ${pc.dim(`(${meta.timeMs}ms)`)}\n`);
for (let i = 0; i < results.length; i++) {
const r = results[i];
const score = (r.score * 100).toFixed(1);
console.log(pc.cyan(`[${i + 1}] ${r.source}`) + pc.dim(` (${r.language || 'unknown'}) — ${score}%`));
if (r.symbols?.length > 0) {
console.log(pc.dim(` Symbols: ${r.symbols.slice(0, 5).join(', ')}`));
}
// Show snippet
const snippet = r.content.slice(0, 200).replace(/\n/g, '\n ');
console.log(pc.dim(' ' + snippet + (r.content.length > 200 ? '...' : '')));
console.log('');
}
}
} catch (err) {
spinner.fail(`Search failed: ${err.message}`);
process.exit(1);
}
});
// Explain code command (context retrieval for code)
program
.command('context-code')
.alias('ctx')
.description('Get contextual information for code from indexed documentation')
.option('--code <snippet>', 'Code snippet to explain')
.option('--file <path>', 'File containing code to explain')
.option('--language <lang>', 'Programming language')
.option('--db <name>', 'MongoDB database name')
.option('--collection <name>', 'Collection with indexed documentation')
.option('--context-limit <n>', 'Number of context documents', (v) => parseInt(v, 10), 5)
.option('--json', 'Output as JSON')
.action(async (opts) => {
const telemetry = require('../lib/telemetry');
telemetry.send('cli_explain_code_run');
const { handleExplainCode } = require('../mcp/tools/workspace');
const fs = require('fs');
let code = opts.code;
// Read from file if provided
if (opts.file && !code) {
try {
code = fs.readFileSync(opts.file, 'utf-8');
if (!opts.language) {
opts.language = path.extname(opts.file).slice(1);
}
} catch (err) {
console.error(`Failed to read file: ${err.message}`);
process.exit(1);
}
}
// Read from stdin if no code provided
if (!code) {
console.error('Provide code via --code, --file, or pipe to stdin');
process.exit(1);
}
const spinner = (await getOra())('Finding relevant context...').start();
try {
const result = await handleExplainCode({
code,
language: opts.language,
db: opts.db,
collection: opts.collection,
contextLimit: opts.contextLimit,
});
spinner.stop();
if (opts.json) {
console.log(JSON.stringify(result.structuredContent, null, 2));
} else {
console.log('\n' + pc.bold('Code Context') + '\n');
console.log(pc.dim('─'.repeat(60)) + '\n');
const context = result.structuredContent.context || [];
if (context.length === 0) {
console.log(pc.yellow('No relevant context found. Try indexing more documentation.'));
} else {
for (const ctx of context) {
console.log(pc.cyan(`[${ctx.source}]`) + pc.dim(` — ${(ctx.score * 100).toFixed(1)}%`));
console.log(ctx.content.slice(0, 500));
console.log(pc.dim('─'.repeat(40)) + '\n');
}
}
}
} catch (err) {
spinner.fail(`Explain failed: ${err.message}`);
process.exit(1);
}
});
}
module.exports = { registerIndexWorkspace };