UNPKG

voyageai-cli

Version:

CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search

193 lines (174 loc) 6.72 kB
'use strict'; 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 };