UNPKG

build-in-public-bot

Version:

AI-powered CLI bot for automating build-in-public tweets with code screenshots

220 lines (217 loc) 9.67 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AIService = void 0; const axios_1 = __importDefault(require("axios")); const errors_1 = require("../utils/errors"); const logger_1 = require("../utils/logger"); const config_1 = require("./config"); class AIService { static instance; configService; constructor() { this.configService = config_1.ConfigService.getInstance(); } async getApiKey() { logger_1.logger.debug('Checking for API keys...'); logger_1.logger.debug(`OPENROUTER_API_KEY exists: ${!!process.env.OPENROUTER_API_KEY}`); logger_1.logger.debug(`OPENAI_API_KEY exists: ${!!process.env.OPENAI_API_KEY}`); logger_1.logger.debug(`ANTHROPIC_API_KEY exists: ${!!process.env.ANTHROPIC_API_KEY}`); const envKey = process.env.OPENROUTER_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY; if (envKey) { logger_1.logger.debug(`Using API key from environment: ${envKey.substring(0, 10)}...`); return envKey; } try { const config = await this.configService.load(); if (config.ai?.apiKey) { logger_1.logger.debug('Using API key from config'); return config.ai.apiKey; } } catch (error) { logger_1.logger.debug('No config found'); } throw new errors_1.AIError('No API key found.\n\n' + 'To fix this:\n' + '1. Run "bip setup api" to configure OpenRouter\n' + '2. Or set an environment variable:\n' + ' export OPENAI_API_KEY="your-key-here"\n' + '3. Or create a .env file with your API key'); } static getInstance() { if (!AIService.instance) { AIService.instance = new AIService(); } return AIService.instance; } async generateTweet(options, config) { try { const apiKey = await this.getApiKey(); const systemPrompt = this.buildSystemPrompt(config.style); const userPrompt = this.buildUserPrompt(options.message, config.style); logger_1.logger.debug('Generating tweet with AI...'); const isOpenAI = apiKey.startsWith('sk-proj') || apiKey.startsWith('sk-'); const isAnthropic = apiKey.startsWith('sk-ant-'); const isOpenRouter = apiKey.startsWith('sk-or-'); logger_1.logger.debug(`API key type - OpenAI: ${isOpenAI}, Anthropic: ${isAnthropic}, OpenRouter: ${isOpenRouter}`); let response; if (isOpenRouter) { logger_1.logger.debug('Using OpenRouter API'); response = await axios_1.default.post('https://openrouter.ai/api/v1/chat/completions', { model: 'anthropic/claude-3-haiku', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], temperature: 0.8, max_tokens: 150 }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://github.com/build-in-public-bot', 'X-Title': 'Build in Public Bot' } }); } else if (isAnthropic || process.env.ANTHROPIC_API_KEY) { response = await axios_1.default.post('https://api.anthropic.com/v1/messages', { model: 'claude-3-sonnet-20240229', messages: [ { role: 'user', content: `${systemPrompt}\n\n${userPrompt}` } ], max_tokens: 150 }, { headers: { 'x-api-key': process.env.ANTHROPIC_API_KEY || apiKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' } }); } else if (isOpenAI || process.env.OPENAI_API_KEY) { response = await axios_1.default.post('https://api.openai.com/v1/chat/completions', { model: 'gpt-3.5-turbo', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], temperature: 0.8, max_tokens: 150 }, { headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY || apiKey}`, 'Content-Type': 'application/json' } }); } else { response = await axios_1.default.post('https://openrouter.ai/api/v1/chat/completions', { model: 'openai/gpt-4-turbo-preview', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], temperature: 0.8, max_tokens: 150 }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://github.com/build-in-public-bot', 'X-Title': 'Build in Public Bot' } }); } let generatedTweet; if (isAnthropic && !isOpenRouter) { if (!response.data.content?.[0]?.text) { throw new Error('No content in Anthropic response'); } generatedTweet = response.data.content[0].text.trim(); } else { if (!response.data.choices?.[0]?.message?.content) { logger_1.logger.error('Invalid response structure:', JSON.stringify(response.data, null, 2)); throw new Error('No content in AI response'); } generatedTweet = response.data.choices[0].message.content.trim(); } return this.validateAndCleanTweet(generatedTweet); } catch (error) { logger_1.logger.error('AI API Error:', error.response?.data || error.message); if (error.response?.status === 401) { throw new errors_1.AIError('Invalid API key. Please check your API key'); } if (error.response?.status === 400) { throw new errors_1.AIError(`Bad request: ${error.response?.data?.error?.message || error.message}`); } throw new errors_1.AIError(`Failed to generate tweet: ${error.message}`, error); } } buildSystemPrompt(style) { const emojiInstructions = style.emojis.frequency === 'none' ? 'Do not use any emojis.' : style.emojis.frequency === 'low' ? 'Use emojis sparingly, maximum 1 per tweet.' : style.emojis.frequency === 'moderate' ? 'Use 1-2 emojis per tweet from this list: ' + style.emojis.preferred.join(' ') : 'Use 2-3 emojis per tweet from this list: ' + style.emojis.preferred.join(' '); const hashtagInstructions = style.hashtags.always.length > 0 ? `Always include these hashtags: ${style.hashtags.always.join(' ')}` : 'No required hashtags.'; return `You are a developer sharing build-in-public updates on Twitter. Your tone is ${style.tone}. ${emojiInstructions} ${hashtagInstructions} Keep tweets under 280 characters. Be authentic, engaging, and specific about technical details. Example tweets for reference: ${Array.isArray(style.examples) ? style.examples.map(ex => `- ${ex}`).join('\n') : '- No examples provided'}`; } buildUserPrompt(message, style) { const contextualHashtags = style.hashtags.contextual.length > 0 ? `Consider using relevant hashtags from: ${style.hashtags.contextual.join(', ')}` : ''; return `Generate a tweet about: ${message} ${contextualHashtags} Return only the tweet text, nothing else.`; } validateAndCleanTweet(tweet) { tweet = tweet.replace(/^["']|["']$/g, ''); if (tweet.length > 280) { const sentences = tweet.match(/[^.!?]+[.!?]+/g) || [tweet]; let result = ''; for (const sentence of sentences) { if ((result + sentence).length <= 280) { result += sentence; } else { break; } } tweet = result || tweet.substring(0, 277) + '...'; } return tweet; } async validateApiKey() { try { const apiKey = await this.getApiKey(); const response = await axios_1.default.get('https://openrouter.ai/api/v1/models', { headers: { 'Authorization': `Bearer ${apiKey}` } }); return response.status === 200; } catch { return false; } } } exports.AIService = AIService; //# sourceMappingURL=ai.js.map