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
JavaScript
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
;