|
#!/usr/bin/env node |
|
|
|
|
|
|
|
import { program } from 'commander'; |
|
import { UniversalLLM } from './index'; |
|
import * as fs from 'fs'; |
|
import * as path from 'path'; |
|
import chalk from 'chalk'; |
|
import ora from 'ora'; |
|
import * as dotenv from 'dotenv'; |
|
import * as os from 'os'; |
|
import * as readline from 'readline'; |
|
import { createSpinner } from 'nanospinner'; |
|
|
|
|
|
dotenv.config(); |
|
|
|
|
|
const packageJson = JSON.parse( |
|
fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8') |
|
); |
|
|
|
|
|
const configDir = path.join(os.homedir(), '.universal-developer'); |
|
const configPath = path.join(configDir, 'config.json'); |
|
let config: any = { |
|
defaultProvider: 'anthropic', |
|
enableTelemetry: true, |
|
apiKeys: {} |
|
}; |
|
|
|
|
|
if (!fs.existsSync(configDir)) { |
|
fs.mkdirSync(configDir, { recursive: true }); |
|
} |
|
|
|
|
|
if (fs.existsSync(configPath)) { |
|
try { |
|
config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); |
|
} catch (error) { |
|
console.error('Error loading config file:', error); |
|
} |
|
} |
|
|
|
|
|
function saveConfig() { |
|
try { |
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); |
|
} catch (error) { |
|
console.error('Error saving config:', error); |
|
} |
|
} |
|
|
|
|
|
program |
|
.name('ud') |
|
.description('Universal Developer CLI - Control LLMs with symbolic runtime commands') |
|
.version(packageJson.version); |
|
|
|
|
|
program |
|
.command('config') |
|
.description('Configure Universal Developer CLI') |
|
.option('-p, --provider <provider>', 'Set default provider (anthropic, openai, qwen, gemini, ollama)') |
|
.option('-k, --key <key>', 'Set API key for the default provider') |
|
.option('--anthropic-key <key>', 'Set API key for Anthropic/Claude') |
|
.option('--openai-key <key>', 'Set API key for OpenAI') |
|
.option('--qwen-key <key>', 'Set API key for Qwen') |
|
.option('--gemini-key <key>', 'Set API key for Google Gemini') |
|
.option('--telemetry <boolean>', 'Enable or disable anonymous telemetry') |
|
.option('-l, --list', 'List current configuration') |
|
.action((options) => { |
|
if (options.list) { |
|
console.log(chalk.bold('\nCurrent Configuration:')); |
|
console.log(`Default Provider: ${chalk.green(config.defaultProvider)}`); |
|
console.log(`Telemetry: ${config.enableTelemetry ? chalk.green('Enabled') : chalk.yellow('Disabled')}`); |
|
console.log('\nAPI Keys:'); |
|
for (const [provider, key] of Object.entries(config.apiKeys)) { |
|
console.log(`${provider}: ${key ? chalk.green('Configured') : chalk.red('Not configured')}`); |
|
} |
|
return; |
|
} |
|
|
|
let changed = false; |
|
|
|
if (options.provider) { |
|
const validProviders = ['anthropic', 'openai', 'qwen', 'gemini', 'ollama']; |
|
if (validProviders.includes(options.provider)) { |
|
config.defaultProvider = options.provider; |
|
changed = true; |
|
console.log(`Default provider set to ${chalk.green(options.provider)}`); |
|
} else { |
|
console.error(`Invalid provider: ${options.provider}. Valid options are: ${validProviders.join(', ')}`); |
|
} |
|
} |
|
|
|
if (options.key) { |
|
if (!config.apiKeys) config.apiKeys = {}; |
|
config.apiKeys[config.defaultProvider] = options.key; |
|
changed = true; |
|
console.log(`API key for ${chalk.green(config.defaultProvider)} has been set`); |
|
} |
|
|
|
|
|
const providerKeys = { |
|
'anthropic': options.anthropicKey, |
|
'openai': options.openaiKey, |
|
'qwen': options.qwenKey, |
|
'gemini': options.geminiKey |
|
}; |
|
|
|
for (const [provider, key] of Object.entries(providerKeys)) { |
|
if (key) { |
|
if (!config.apiKeys) config.apiKeys = {}; |
|
config.apiKeys[provider] = key; |
|
changed = true; |
|
console.log(`API key for ${chalk.green(provider)} has been set`); |
|
} |
|
} |
|
|
|
if (options.telemetry !== undefined) { |
|
const enableTelemetry = options.telemetry === 'true'; |
|
config.enableTelemetry = enableTelemetry; |
|
changed = true; |
|
console.log(`Telemetry ${enableTelemetry ? chalk.green('enabled') : chalk.yellow('disabled')}`); |
|
} |
|
|
|
if (changed) { |
|
saveConfig(); |
|
console.log(chalk.bold('\nConfiguration saved!')); |
|
} else { |
|
console.log('No changes made. Use --help to see available options.'); |
|
} |
|
}); |
|
|
|
|
|
async function getPipedInput(): Promise<string | null> { |
|
if (process.stdin.isTTY) { |
|
return null; |
|
} |
|
|
|
return new Promise((resolve) => { |
|
let data = ''; |
|
process.stdin.on('readable', () => { |
|
const chunk = process.stdin.read(); |
|
if (chunk !== null) { |
|
data += chunk; |
|
} |
|
}); |
|
|
|
process.stdin.on('end', () => { |
|
resolve(data); |
|
}); |
|
}); |
|
} |
|
|
|
|
|
function getApiKey(provider: string): string { |
|
|
|
if (config.apiKeys && config.apiKeys[provider]) { |
|
return config.apiKeys[provider]; |
|
} |
|
|
|
|
|
const envVarName = `${provider.toUpperCase()}_API_KEY`; |
|
const apiKey = process.env[envVarName]; |
|
|
|
if (!apiKey) { |
|
console.error(chalk.red(`Error: No API key found for ${provider}.`)); |
|
console.log(`Please set your API key using: ud config --${provider}-key <your-api-key>`); |
|
console.log(`Or set the ${envVarName} environment variable.`); |
|
process.exit(1); |
|
} |
|
|
|
return apiKey; |
|
} |
|
|
|
|
|
program |
|
.command('interactive') |
|
.alias('i') |
|
.description('Start an interactive session') |
|
.option('-p, --provider <provider>', 'LLM provider to use') |
|
.option('-m, --model <model>', 'Model to use') |
|
.action(async (options) => { |
|
const provider = options.provider || config.defaultProvider; |
|
const apiKey = getApiKey(provider); |
|
|
|
const llm = new UniversalLLM({ |
|
provider, |
|
apiKey, |
|
model: options.model, |
|
telemetryEnabled: config.enableTelemetry |
|
}); |
|
|
|
console.log(chalk.bold('\nUniversal Developer Interactive Mode')); |
|
console.log(chalk.dim(`Using provider: ${provider}`)); |
|
console.log(chalk.dim('Type /exit or Ctrl+C to quit')); |
|
console.log(chalk.dim('Available commands: /think, /fast, /loop, /reflect, /fork, /collapse\n')); |
|
|
|
const rl = readline.createInterface({ |
|
input: process.stdin, |
|
output: process.stdout |
|
}); |
|
|
|
let conversationHistory: { role: string, content: string }[] = []; |
|
|
|
const promptUser = () => { |
|
rl.question('> ', async (input) => { |
|
if (input.toLowerCase() === '/exit') { |
|
rl.close(); |
|
return; |
|
} |
|
|
|
|
|
conversationHistory.push({ |
|
role: 'user', |
|
content: input |
|
}); |
|
|
|
const spinner = createSpinner('Generating response...').start(); |
|
|
|
try { |
|
const response = await llm.generate({ |
|
messages: conversationHistory |
|
}); |
|
|
|
spinner.success(); |
|
console.log(`\n${chalk.blue('Assistant:')} ${response}\n`); |
|
|
|
|
|
conversationHistory.push({ |
|
role: 'assistant', |
|
content: response |
|
}); |
|
} catch (error) { |
|
spinner.error(); |
|
console.error(`Error: ${error.message}`); |
|
} |
|
|
|
promptUser(); |
|
}); |
|
}; |
|
|
|
console.log(chalk.blue('Assistant:') + ' Hello! How can I help you today?\n'); |
|
conversationHistory.push({ |
|
role: 'assistant', |
|
content: 'Hello! How can I help you today?' |
|
}); |
|
|
|
promptUser(); |
|
}); |
|
|
|
|
|
const symbolicCommands = [ |
|
{ name: 'think', description: 'Generate response using deep reasoning' }, |
|
{ name: 'fast', description: 'Generate quick, concise response' }, |
|
{ name: 'loop', description: 'Generate iteratively refined response' }, |
|
{ name: 'reflect', description: 'Generate response with self-reflection' }, |
|
{ name: 'fork', description: 'Generate multiple alternative responses' }, |
|
{ name: 'collapse', description: 'Generate response using default behavior' } |
|
]; |
|
|
|
symbolicCommands.forEach(cmd => { |
|
program |
|
.command(cmd.name) |
|
.description(cmd.description) |
|
.argument('[prompt]', 'The prompt to send to the LLM') |
|
.option('-p, --provider <provider>', 'LLM provider to use') |
|
.option('-m, --model <model>', 'Model to use') |
|
.option('-s, --system <prompt>', 'System prompt to use') |
|
.option('-i, --iterations <number>', 'Number of iterations (for loop command)') |
|
.option('-c, --count <number>', 'Number of alternatives (for fork command)') |
|
.action(async (promptArg, options) => { |
|
|
|
const provider = options.provider || config.defaultProvider; |
|
const apiKey = getApiKey(provider); |
|
|
|
|
|
const llm = new UniversalLLM({ |
|
provider, |
|
apiKey, |
|
model: options.model, |
|
telemetryEnabled: config.enableTelemetry |
|
}); |
|
|
|
|
|
const pipedInput = await getPipedInput(); |
|
|
|
|
|
let prompt = promptArg || ''; |
|
if (pipedInput) { |
|
prompt = prompt ? `${prompt}\n\n${pipedInput}` : pipedInput; |
|
} |
|
|
|
|
|
if (!prompt) { |
|
console.error('Error: Prompt is required.'); |
|
console.log(`Usage: ud ${cmd.name} "Your prompt here"`); |
|
console.log('Or pipe content: cat file.txt | ud ${cmd.name}'); |
|
process.exit(1); |
|
} |
|
|
|
|
|
let commandString = `/${cmd.name}`; |
|
|
|
|
|
if (cmd.name === 'loop' && options.iterations) { |
|
commandString += ` --iterations=${options.iterations}`; |
|
} else if (cmd.name === 'fork' && options.count) { |
|
commandString += ` --count=${options.count}`; |
|
} |
|
|
|
|
|
const fullPrompt = `${commandString} ${prompt}`; |
|
|
|
|
|
console.log(chalk.dim(`Using provider: ${provider}`)); |
|
const spinner = createSpinner('Generating response...').start(); |
|
|
|
try { |
|
const response = await llm.generate({ |
|
systemPrompt: options.system, |
|
prompt: fullPrompt |
|
}); |
|
|
|
spinner.success(); |
|
console.log('\n' + response + '\n'); |
|
} catch (error) { |
|
spinner.error(); |
|
console.error(`Error: ${error.message}`); |
|
process.exit(1); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
program |
|
.arguments('[prompt]') |
|
.option('-p, --provider <provider>', 'LLM provider to use') |
|
.option('-m, --model <model>', 'Model to use') |
|
.option('-s, --system <prompt>', 'System prompt to use') |
|
.option('-c, --command <command>', 'Symbolic command to use') |
|
.action(async (promptArg, options) => { |
|
if (!promptArg && !process.stdin.isTTY) { |
|
|
|
const pipedInput = await getPipedInput(); |
|
if (pipedInput) { |
|
promptArg = pipedInput; |
|
} |
|
} |
|
|
|
if (!promptArg) { |
|
|
|
program.commands.find(cmd => cmd.name() === 'interactive').action(options); |
|
return; |
|
} |
|
|
|
|
|
const provider = options.provider || config.defaultProvider; |
|
const apiKey = getApiKey(provider); |
|
|
|
|
|
const llm = new UniversalLLM({ |
|
provider, |
|
apiKey, |
|
model: options.model, |
|
telemetryEnabled: config.enableTelemetry |
|
}); |
|
|
|
|
|
const command = options.command || 'think'; |
|
|
|
|
|
const fullPrompt = `/${command} ${promptArg}`; |
|
|
|
|
|
console.log(chalk.dim(`Using provider: ${provider}`)); |
|
const spinner = createSpinner('Generating response...').start(); |
|
|
|
try { |
|
const response = await llm.generate({ |
|
systemPrompt: options.system, |
|
prompt: fullPrompt |
|
}); |
|
|
|
spinner.success(); |
|
console.log('\n' + response + '\n'); |
|
} catch (error) { |
|
spinner.error(); |
|
console.error(`Error: ${error.message}`); |
|
process.exit(1); |
|
} |
|
}); |
|
|
|
|
|
program.parse(); |
|
|