voyageai-cli
Version:
CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search
193 lines (174 loc) • 6.72 kB
JavaScript
;
const fs = require('fs');
const {
CONFIG_PATH,
KEY_MAP,
SECRET_KEYS,
loadConfig,
saveConfig,
getConfigValue,
setConfigValue,
deleteConfigValue,
maskSecret,
} = require('../lib/config');
const ui = require('../lib/ui');
const VALID_KEYS = Object.keys(KEY_MAP);
const INTERNAL_KEYS = new Set(['telemetryNoticeShown', 'telemetryNoticeShownAt']);
/**
* Register the config command on a Commander program.
* @param {import('commander').Command} program
*/
function registerConfig(program) {
const configCmd = program
.command('config')
.description('Manage persistent configuration (~/.vai/config.json)');
// ── config set <key> [value] ──
configCmd
.command('set <key> [value]')
.description('Set a config value (omit value to read from stdin)')
.option('--stdin', 'Read value from stdin (avoids shell history exposure)')
.action(async (key, value, opts) => {
const internalKey = KEY_MAP[key];
if (!internalKey) {
console.error(ui.error(`Unknown config key "${ui.cyan(key)}".`));
console.error(`Valid keys: ${VALID_KEYS.join(', ')}`);
process.exit(1);
}
// Read from stdin if no value provided or --stdin flag
if (!value || opts.stdin) {
if (process.stdin.isTTY && !value) {
// Interactive: prompt without echo for secrets
const isSecret = SECRET_KEYS.has(internalKey);
if (isSecret) {
process.stderr.write(`Enter ${key}: `);
} else {
process.stderr.write(`Enter ${key}: `);
}
}
if (!value) {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
value = Buffer.concat(chunks).toString('utf-8').trim();
}
}
if (!value) {
console.error(ui.error('No value provided.'));
process.exit(1);
}
// Parse numeric values for dimensions
const storedValue = key === 'default-dimensions' ? parseInt(value, 10) : value;
setConfigValue(internalKey, storedValue);
console.log(ui.success(`Set ${ui.cyan(key)} = ${SECRET_KEYS.has(internalKey) ? ui.dim(maskSecret(String(storedValue))) : storedValue}`));
// When setting an API key, auto-configure the matching base URL
if (internalKey === 'apiKey') {
const { identifyKey, ATLAS_API_BASE, VOYAGE_API_BASE } = require('../lib/api');
const keyInfo = identifyKey(storedValue);
const currentBase = getConfigValue('baseUrl');
if (keyInfo.type === 'atlas') {
if (currentBase !== ATLAS_API_BASE) {
setConfigValue('baseUrl', ATLAS_API_BASE);
console.log('');
console.log(ui.success(`Auto-configured base URL → ${ui.cyan(ATLAS_API_BASE)}`));
}
console.log('');
console.log(` 🔑 ${ui.bold('MongoDB Atlas key detected')} (al-*)`);
console.log(` Endpoint: ${ui.cyan(ATLAS_API_BASE)}`);
console.log(` Atlas provides Voyage AI models with Atlas-native billing.`);
console.log(` All vai features work identically on both endpoints.`);
} else if (keyInfo.type === 'voyage') {
if (currentBase !== VOYAGE_API_BASE) {
setConfigValue('baseUrl', VOYAGE_API_BASE);
console.log('');
console.log(ui.success(`Auto-configured base URL → ${ui.cyan(VOYAGE_API_BASE)}`));
}
console.log('');
console.log(` 🔑 ${ui.bold('Voyage AI key detected')} (pa-*)`);
console.log(` Endpoint: ${ui.cyan(VOYAGE_API_BASE)}`);
} else {
console.log('');
console.log(` ⚠️ Unrecognized key prefix. Expected ${ui.cyan('al-*')} (Atlas) or ${ui.cyan('pa-*')} (Voyage AI).`);
console.log(` Make sure your base URL matches: ${ui.cyan('vai config set base-url <url>')}`);
}
}
});
// ── config get [key] ──
configCmd
.command('get [key]')
.description('Get a config value (or all if no key)')
.action((key) => {
if (key) {
const internalKey = KEY_MAP[key];
if (!internalKey) {
console.error(ui.error(`Unknown config key "${ui.cyan(key)}".`));
console.error(`Valid keys: ${VALID_KEYS.join(', ')}`);
process.exit(1);
}
const value = getConfigValue(internalKey);
if (value === undefined) {
console.log(`${ui.cyan(key)}: ${ui.dim('(not set)')}`);
} else {
const display = SECRET_KEYS.has(internalKey) ? ui.dim(maskSecret(String(value))) : value;
console.log(`${ui.cyan(key)}: ${display}`);
}
} else {
// Show all config
const config = loadConfig();
if (Object.keys(config).length === 0) {
console.log(ui.dim('No configuration set.'));
console.log(`Run: ${ui.cyan('vai config set <key> <value>')}`);
return;
}
// Build a reverse map: internalKey → cliKey
const reverseMap = {};
for (const [cliKey, intKey] of Object.entries(KEY_MAP)) {
reverseMap[intKey] = cliKey;
}
for (const [intKey, value] of Object.entries(config)) {
if (INTERNAL_KEYS.has(intKey)) continue;
const cliKey = reverseMap[intKey] || intKey;
const display = SECRET_KEYS.has(intKey) ? ui.dim(maskSecret(String(value))) : value;
console.log(`${ui.cyan(cliKey)}: ${display}`);
}
}
});
// ── config delete <key> ──
configCmd
.command('delete <key>')
.description('Remove a config value')
.action((key) => {
const internalKey = KEY_MAP[key];
if (!internalKey) {
console.error(ui.error(`Unknown config key "${ui.cyan(key)}".`));
console.error(`Valid keys: ${VALID_KEYS.join(', ')}`);
process.exit(1);
}
deleteConfigValue(internalKey);
console.log(ui.success(`Deleted ${ui.cyan(key)}`));
});
// ── config path ──
configCmd
.command('path')
.description('Print the config file path')
.action(() => {
console.log(CONFIG_PATH);
});
// ── config reset ──
configCmd
.command('reset')
.description('Delete the entire config file')
.action(() => {
try {
fs.unlinkSync(CONFIG_PATH);
console.log(ui.success(`Config file deleted: ${ui.dim(CONFIG_PATH)}`));
} catch (err) {
if (err.code === 'ENOENT') {
console.log(ui.dim('No config file found. Nothing to reset.'));
} else {
throw err;
}
}
});
}
module.exports = { registerConfig };