UNPKG

@meldscience/meld

Version:

pipeable one-shot prompt scripting toolkit

337 lines (303 loc) 12.3 kB
#!/usr/bin/env -S node --no-warnings --no-deprecation import { Command } from 'commander'; import { readFileSync, writeFileSync } from 'fs'; import { Oneshot } from '../src/oneshot'; import { AnthropicProvider } from '../src/providers/anthropic'; import { OpenAIProvider } from '../src/providers/openai'; import { loadConfig, checkAndPromptConfig, setupConfig } from '../src/config'; import { parse } from 'yaml'; import { createInterface } from 'readline'; import { existsSync } from 'fs'; const program = new Command(); program .name('oneshot') .description('Send AIs prompts with optional variations') .argument('[file]', 'Path to prompt file or quoted text') .option('-m, --model <model>', 'AI model(s) to use (e.g. claude-3 or "gpt-4 claude-3")') .option('-o, --outfile <path>', 'Save output to file') .option('--system <prompt>', 'System prompt') .option('--system-file <path>', 'System prompt from file') .option('--variations <json>', 'Variation prompts as JSON array') .option('--variations-file <path>', 'Variation prompts from file (JSON or YAML)') .option('--iterations <number>', 'Number of responses per variation', '1') .option('--default [model]', 'Set default model for future oneshot commands') .addHelpText('after', ` Examples: $ oneshot "Hello, how are you?" Send text prompt using default model $ oneshot 'Tell me a story' Send text prompt using default model $ oneshot prompt.md Send file prompt using default model $ oneshot -m claude-3 'Tell me a story' Send text prompt to Claude $ oneshot -m "gpt-4 claude-3" prompt.md Send to multiple models $ oneshot prompt.md --system "..." Add system prompt $ oneshot prompt.md -o response.md Save to file $ oneshot prompt.md --variations-file roles.yaml $ oneshot --default Choose default model interactively $ oneshot --default 4o Set 4o as default model For more examples and documentation: https://github.com/meldscience/oneshot#readme`) .showHelpAfterError('(add --help for additional information)') .action((promptFile: string | undefined, options: any) => { // Commander will handle missing required args }); program.parse(); async function main() { // Load config first to check for aliases and default model const config = loadConfig(); const [inputFile] = program.args; const options = program.opts(); // Handle setting default model if (options.default !== undefined) { if (options.default === true) { // Interactive model selection async function selectModel() { const rl = createInterface({ input: process.stdin, output: process.stdout }); console.log('\nAvailable models:'); console.log(' Claude models:'); console.log(' 1. claude-3-5-sonnet-latest'); console.log(' 2. claude-3-opus-latest'); console.log(' 3. claude-3-5-haiku-latest'); console.log('\n GPT models:'); console.log(' 4. chatgpt-4o-latest'); console.log(' 5. o1-preview'); console.log(' 6. o1-mini'); if (config.modelAliases) { console.log('\n Aliases:'); Object.entries(config.modelAliases).forEach(([alias, target]) => { console.log(` ${alias}${target}`); }); } const currentDefault = config.defaultOneshotModel; console.log(`\nCurrent default: ${currentDefault || '(none)'}`); const answer = await new Promise<string>(resolve => { rl.question('\nEnter model name or number: ', resolve); }); rl.close(); let selectedModel = answer.trim(); // Map numbers to models const modelMap: {[key: string]: string} = { '1': 'claude-3-5-sonnet-latest', '2': 'claude-3-opus-latest', '3': 'claude-3-5-haiku-latest', '4': 'chatgpt-4o-latest', '5': 'o1-preview', '6': 'o1-mini' }; if (modelMap[selectedModel]) { selectedModel = modelMap[selectedModel]; } setupConfig({ defaultOneshotModel: selectedModel }); console.log(`✅ Set default model to: ${selectedModel}`); process.exit(0); } await selectModel().catch(error => { console.error('❌ Error:', error.message); process.exit(1); }); return; } else { setupConfig({ defaultOneshotModel: options.default }); console.log(`✅ Set default model to: ${options.default}`); process.exit(0); } } // Require file argument if not setting default if (!inputFile) { console.error('❌ Error: Missing required argument: file'); console.error('(add --help for additional information)'); process.exit(1); } // Get model from option or default let model = options.model; if (!model) { const defaultModel = config.defaultOneshotModel; if (!defaultModel) { console.error('❌ Error: No model specified and no default model configured'); console.error('Either provide a model with -m/--model or set a default with:'); console.error(' oneshot --default <model>'); process.exit(1); } model = defaultModel; } const models = model.split(/\s+/); try { // Validate and resolve models for (const model of models) { // Check if it's an alias first const resolvedModel = config.modelAliases?.[model] || model; const isAlias = model in (config.modelAliases || {}); if (!resolvedModel.startsWith('claude') && !resolvedModel.startsWith('gpt') && !resolvedModel.startsWith('chatgpt') && !resolvedModel.startsWith('o1')) { if (!isAlias) { // This is not an alias console.error(`❌ Error: Unsupported model: ${model}`); console.error('Supported models start with: claude-, gpt-, chatgpt-, o1'); console.error('\nOr use these aliases:'); if (config.modelAliases) { Object.entries(config.modelAliases).forEach(([alias, target]) => { console.error(` ${alias}${target}`); }); } } else { // This is an alias that resolved to an invalid model console.error(`❌ Error: Unsupported model: ${resolvedModel} (from alias '${model}')`); console.error('Supported models start with: claude-, gpt-, chatgpt-, o1'); } process.exit(1); } } // Check for required API keys checkAndPromptConfig(models.map((m: string) => config.modelAliases?.[m] || m)); // Get prompt content - either from file or direct input let promptContent: string; try { if (existsSync(inputFile)) { promptContent = readFileSync(inputFile, 'utf-8'); } else { promptContent = inputFile; } } catch (error: any) { console.error('❌ Error: Failed to read prompt:', error.message); process.exit(1); } // Read system prompt if specified let systemPrompt = options.system; if (options.systemFile) { try { systemPrompt = readFileSync(options.systemFile, 'utf-8'); } catch (error: any) { if (error.code === 'ENOENT') { console.error('❌ Error: System prompt file not found:', options.systemFile); process.exit(1); } throw error; } } // Parse variations if specified let variations: string[] | undefined; if (options.variations) { try { variations = JSON.parse(options.variations); if (!Array.isArray(variations)) { console.error('❌ Error: Variations must be a JSON array'); process.exit(1); } } catch (error) { console.error('❌ Error: Invalid JSON in variations argument'); console.error('Example: --variations \'["Perspective 1", "Perspective 2"]\''); process.exit(1); } } else if (options.variationsFile) { try { const content = readFileSync(options.variationsFile, 'utf-8'); if (options.variationsFile.endsWith('.json')) { variations = JSON.parse(content); if (!Array.isArray(variations)) { console.error('❌ Error: JSON variations file must contain an array'); process.exit(1); } } else if (options.variationsFile.endsWith('.yaml') || options.variationsFile.endsWith('.yml')) { const yamlContent = parse(content); variations = Array.isArray(yamlContent) ? yamlContent : Object.values(yamlContent); } else { console.error('❌ Error: Variations file must be .json, .yaml, or .yml'); process.exit(1); } } catch (error: any) { if (error.code === 'ENOENT') { console.error('❌ Error: Variations file not found:', options.variationsFile); } else { console.error('❌ Error: Failed to parse variations file:', error.message || 'Invalid format'); } process.exit(1); } } // Process each model const allResponses = []; for (const model of models) { // Get resolved model name const resolvedModel = config.modelAliases?.[model] || model; // Select provider based on resolved model let provider; if (resolvedModel.startsWith('claude')) { if (!config.anthropicApiKey) { console.error('❌ Error: Anthropic API key is required for Claude models'); console.error('Run: meld-config --anthropic-key <your-key>'); process.exit(1); } provider = new AnthropicProvider(config.anthropicApiKey); } else if (resolvedModel.startsWith('gpt') || resolvedModel.startsWith('o1') || resolvedModel.startsWith('chatgpt')) { if (!config.openaiApiKey) { console.error('❌ Error: OpenAI API key is required for GPT models'); console.error('Run: meld-config --openai-key <your-key>'); process.exit(1); } provider = new OpenAIProvider(config.openaiApiKey); } else { console.error(`❌ Error: Unsupported model: ${model}`); console.error('Supported models start with: claude-, gpt-, chatgpt-, o1'); process.exit(1); } // Create and run oneshot console.log(`🤖 Sending prompt to ${model} (${resolvedModel})`); const oneshot = new Oneshot({ model, promptFile: inputFile, system: systemPrompt, variations, iterations: parseInt(options.iterations, 10) }, provider); const responses = await oneshot.process(); allResponses.push(...responses.map(r => ({ ...r, model }))); } // Format output const output = allResponses.map((r) => { const modelHeader = `# Model: ${r.model}`; let variationHeader = ''; if (r.variation) { if (typeof r.variation === 'string') { variationHeader = `## Variation: ${r.variation}`; } else { const parts = []; if (r.variation.prompt) parts.push(`Prompt: ${r.variation.prompt}`); if (r.variation.system) parts.push(`System: ${r.variation.system}`); if (r.variation.model) parts.push(`Model: ${r.variation.model}`); if (parts.length > 0) { variationHeader = `## Variation:\n${parts.map(p => `- ${p}`).join('\n')}`; } } } const headers = [modelHeader, variationHeader].filter(Boolean).join('\n'); return `${headers}\n\n${r.response}`; }).join('\n\n---\n\n'); // Write output if (options.outfile) { try { writeFileSync(options.outfile, output); console.log(`📝 Response written to ${options.outfile}`); } catch (error: any) { console.error('❌ Error: Failed to write output file:', error.message); process.exit(1); } } else { console.log('\n' + output); } } catch (error: any) { console.error('❌ Error:', error.message); process.exit(1); } } // Start the program main().catch(error => { console.error('❌ Error:', error.message); process.exit(1); });