voyageai-cli
Version:
CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search
223 lines (201 loc) • 7.07 kB
JavaScript
;
const fs = require('fs');
const path = require('path');
const { loadProject } = require('../lib/project');
const { renderTemplate, buildContext, listTemplates, listTargets } = require('../lib/codegen');
const ui = require('../lib/ui');
/**
* Component to template mapping for each target.
*/
const COMPONENT_MAP = {
vanilla: {
client: 'client.js',
connection: 'connection.js',
retrieval: 'retrieval.js',
ingest: 'ingest.js',
'search-api': 'search-api.js',
},
nextjs: {
client: 'lib-voyage.js',
connection: 'lib-mongo.js',
retrieval: 'route-search.js', // Route includes retrieval logic
ingest: 'route-ingest.js',
'search-api': 'route-search.js',
'search-page': 'page-search.jsx',
theme: 'theme.js',
layout: 'layout.jsx',
},
python: {
client: 'voyage_client.py',
connection: 'mongo_client.py',
retrieval: 'app.py', // App includes retrieval routes
ingest: 'chunker.py',
'search-api': 'app.py',
},
};
/**
* Suggested output filenames for each component.
*/
const OUTPUT_NAMES = {
vanilla: {
client: 'lib/voyage.js',
connection: 'lib/mongodb.js',
retrieval: 'lib/retrieval.js',
ingest: 'lib/ingest.js',
'search-api': 'lib/search-api.js',
},
nextjs: {
client: 'lib/voyage.js',
connection: 'lib/mongodb.js',
retrieval: 'app/api/search/route.js',
ingest: 'app/api/ingest/route.js',
'search-api': 'app/api/search/route.js',
'search-page': 'app/search/page.jsx',
theme: 'lib/theme.js',
layout: 'app/layout.jsx',
},
python: {
client: 'voyage_client.py',
connection: 'mongo_client.py',
retrieval: 'app.py',
ingest: 'chunker.py',
'search-api': 'app.py',
},
};
/**
* Auto-detect target from project files.
*/
function detectTarget() {
const cwd = process.cwd();
// Check for Next.js
if (
fs.existsSync(path.join(cwd, 'next.config.js')) ||
fs.existsSync(path.join(cwd, 'next.config.mjs')) ||
fs.existsSync(path.join(cwd, 'next.config.ts'))
) {
return 'nextjs';
}
// Check for Python
if (
fs.existsSync(path.join(cwd, 'requirements.txt')) ||
fs.existsSync(path.join(cwd, 'pyproject.toml')) ||
fs.existsSync(path.join(cwd, 'setup.py'))
) {
return 'python';
}
// Default to vanilla Node.js
return 'vanilla';
}
/**
* Get available components for a target.
*/
function getComponents(target) {
return Object.keys(COMPONENT_MAP[target] || {});
}
/**
* Register the generate command.
* @param {import('commander').Command} program
*/
function registerGenerate(program) {
program
.command('generate [component]')
.alias('gen')
.description('Generate code snippets for RAG applications')
.option('-t, --target <target>', 'Target framework: vanilla, nextjs, python')
.option('-m, --model <model>', 'Override embedding model')
.option('--db <database>', 'Override database name')
.option('--collection <name>', 'Override collection name')
.option('--field <name>', 'Override embedding field name')
.option('--index <name>', 'Override vector index name')
.option('-d, --dimensions <n>', 'Override dimensions', parseInt)
.option('--no-rerank', 'Omit reranking from generated code')
.option('--rerank-model <model>', 'Rerank model to use')
.option('--json', 'Machine-readable output with filename')
.option('-l, --list', 'List available components')
.option('-q, --quiet', 'Suppress hints and metadata')
.action(async (component, opts) => {
const telemetry = require('../lib/telemetry');
try {
telemetry.send('cli_generate', { provider: opts.target, model: opts.model });
// Determine target
const target = opts.target || detectTarget();
// Validate target
const validTargets = ['vanilla', 'nextjs', 'python'];
if (!validTargets.includes(target)) {
console.error(ui.error(`Invalid target: ${target}`));
console.error(ui.dim(` Valid targets: ${validTargets.join(', ')}`));
process.exit(1);
}
// List components
if (opts.list || !component) {
console.log('');
console.log(ui.bold(`Available components for ${target}:`));
console.log('');
const components = getComponents(target);
for (const comp of components) {
const outputName = OUTPUT_NAMES[target][comp];
console.log(` ${ui.cyan(comp.padEnd(15))} → ${ui.dim(outputName)}`);
}
console.log('');
console.log(ui.dim('Usage: vai generate <component> [--target <target>]'));
console.log(ui.dim(' vai generate retrieval > lib/retrieval.js'));
console.log('');
return;
}
// Validate component
const components = getComponents(target);
if (!components.includes(component)) {
console.error(ui.error(`Unknown component: ${component}`));
console.error(ui.dim(` Available: ${components.join(', ')}`));
process.exit(1);
}
// Load project config
let project = {};
try {
project = loadProject();
} catch (e) {
// No .vai.json, use defaults
if (!opts.quiet) {
console.error(ui.warn('No .vai.json found, using defaults'));
}
}
// Build context with overrides
const context = buildContext(project, {
model: opts.model,
db: opts.db,
collection: opts.collection,
field: opts.field,
index: opts.index,
dimensions: opts.dimensions,
rerank: opts.rerank,
rerankModel: opts.rerankModel,
projectName: path.basename(process.cwd()),
});
// Get template name (strip the output extension, keep for template lookup)
const templateFile = COMPONENT_MAP[target][component];
const templateName = templateFile.replace(/\.(js|jsx|py)$/, '');
// Render template
const output = renderTemplate(target, templateName, context);
// Output
if (opts.json) {
const filename = OUTPUT_NAMES[target][component];
console.log(JSON.stringify({ filename, content: output }, null, 2));
} else {
// Output to stdout for piping
console.log(output);
// Hints on stderr
if (!opts.quiet) {
const filename = OUTPUT_NAMES[target][component];
console.error('');
console.error(ui.dim(`# Generated ${component} for ${target}`));
console.error(ui.dim(`# Suggested: vai generate ${component} > ${filename}`));
}
}
} catch (err) {
console.error(ui.error(err.message));
if (process.env.DEBUG) console.error(err.stack);
process.exit(1);
}
});
}
module.exports = { registerGenerate, detectTarget, getComponents };