import "dotenv/config"; import Kernel from "@onkernel/sdk"; import { readFileSync, writeFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; const __dirname = dirname(fileURLToPath(import.meta.url)); const PROMPTS_FILE = join(__dirname, "prompts.json"); function loadPrompts() { return JSON.parse(readFileSync(PROMPTS_FILE, "utf-8")); } function historyPath(name) { return join(__dirname, `history-${name}.json`); } function loadHistory(name) { try { return JSON.parse(readFileSync(historyPath(name), "utf-8")); } catch { return []; } } function saveHistory(name, history) { writeFileSync(historyPath(name), JSON.stringify(history, null, 2)); } function shouldRun(promptConfig) { const history = loadHistory(promptConfig.name); const now = new Date(); if (promptConfig.frequency === "daily") { return true; } if (promptConfig.frequency === "random" && promptConfig.minDays) { if (history.length > 0) { const lastPost = new Date(history[history.length - 1].date); const daysSinceLast = (now - lastPost) / (1000 * 60 * 60 * 24); if (daysSinceLast < promptConfig.minDays) return false; } // Random chance so it doesn't always fire on the minimum day return Math.random() < 0.5; } return true; } function buildPrompt(promptConfig, history) { let prompt = promptConfig.prompt; if (history.length > 0) { const recent = history.slice(-20).map((h) => h.tweet).join("\n---\n"); prompt += "\n\nHere are previous tweets. Do NOT repeat or closely resemble any of these:\n\n" + recent; } return prompt; } async function generateTweet(promptConfig, history) { const res = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.ANTHROPIC, "anthropic-version": "2023-06-01", }, body: JSON.stringify({ model: "claude-sonnet-4-6", max_tokens: 280, messages: [{ role: "user", content: buildPrompt(promptConfig, history) }], }), }); if (!res.ok) { throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`); } const data = await res.json(); return data.content[0].text.trim(); } async function exec(kernel, sessionId, code, timeout = 30) { const result = await kernel.browsers.playwright.execute(sessionId, { code, timeout_sec: timeout, }); return result; } async function readPage(kernel, sessionId) { const result = await exec(kernel, sessionId, ` await page.waitForTimeout(2000); const inputs = await page.locator('input').all(); const info = []; for (const inp of inputs) { const type = await inp.getAttribute('type'); const name = await inp.getAttribute('name'); info.push({ type, name }); } const texts = await page.locator('h1, h2, span').allTextContents(); return { url: page.url(), inputs: info, texts: texts.filter(t => t.trim()).slice(0, 20) }; `); if (!result.success) throw new Error("readPage failed: " + result.error); return result.result; } async function login(kernel, sessionId) { const xAcct = JSON.stringify(process.env.X_ACCT); const xPw = JSON.stringify(process.env.X_PW); const xEmail = JSON.stringify(process.env.X_EMAIL); const xPhone = JSON.stringify(process.env.X_PHONE); await exec(kernel, sessionId, ` await page.goto('https://x.com/login', { waitUntil: 'networkidle' }); await page.waitForTimeout(3000); `); const r1 = await exec(kernel, sessionId, ` const input = page.locator('input[autocomplete="username"]'); await input.waitFor({ state: 'visible', timeout: 10000 }); await input.click(); await input.fill(${xAcct}); await page.waitForTimeout(500); const nextBtn = page.locator('[role="button"]').filter({ hasText: 'Next' }); await nextBtn.click(); await page.waitForTimeout(5000); `, 30); console.log("Username:", r1.success ? "ok" : r1.error); if (!r1.success) throw new Error("Username failed: " + r1.error); let pageState = await readPage(kernel, sessionId); console.log("After username:", JSON.stringify(pageState, null, 2)); const hasPassword = pageState.inputs.some((i) => i.type === "password"); const pageText = pageState.texts.join(" ").toLowerCase(); if (!hasPassword && (pageText.includes("unusual login") || pageText.includes("verify") || pageText.includes("confirm"))) { let verifyValue; if (pageText.includes("phone number or email")) { verifyValue = xPhone; } else if (pageText.includes("email")) { verifyValue = xEmail; } else { verifyValue = xPhone; } console.log("Verification required"); const rv = await exec(kernel, sessionId, ` const input = page.locator('input[type="text"]'); await input.waitFor({ state: 'visible', timeout: 10000 }); await input.click(); await input.fill(${verifyValue}); await page.waitForTimeout(500); const nextBtn = page.locator('[role="button"]').filter({ hasText: 'Next' }); await nextBtn.click(); await page.waitForTimeout(5000); `, 30); if (!rv.success) throw new Error("Verification failed: " + rv.error); pageState = await readPage(kernel, sessionId); } const r2 = await exec(kernel, sessionId, ` const input = page.locator('input[type="password"]'); await input.waitFor({ state: 'visible', timeout: 10000 }); await input.click(); await input.fill(${xPw}); await page.waitForTimeout(500); const loginBtn = page.getByTestId('LoginForm_Login_Button'); await loginBtn.click(); await page.waitForTimeout(5000); `, 30); if (!r2.success) throw new Error("Password failed: " + r2.error); pageState = await readPage(kernel, sessionId); console.log("Logged in:", pageState.url); return pageState.url; } async function postTweet(kernel, sessionId, tweetText) { const composeResult = await exec(kernel, sessionId, ` const composeBtn = page.getByTestId('SideNav_NewTweet_Button'); await composeBtn.waitFor({ state: 'visible', timeout: 10000 }); await composeBtn.click(); await page.waitForTimeout(2000); const editor = page.getByRole('textbox'); await editor.waitFor({ state: 'visible', timeout: 10000 }); return 'ready'; `, 30); if (!composeResult.success) throw new Error("Compose failed: " + composeResult.error); const tweetStr = JSON.stringify(tweetText); const post = await exec(kernel, sessionId, ` const editor = page.getByRole('textbox'); await editor.click(); await page.keyboard.type(${tweetStr}, { delay: 30 }); await page.waitForTimeout(1000); const postButton = page.getByTestId('tweetButton'); await postButton.click(); await page.waitForTimeout(5000); return { url: page.url() }; `, 120); if (!post.success) throw new Error(post.error || "Failed to post"); return post; } async function main() { const promptName = process.argv[2]; if (!promptName) { console.error("Usage: node bot.js "); console.error("Available:", loadPrompts().map((p) => p.name).join(", ")); process.exit(1); } const prompts = loadPrompts(); const promptConfig = prompts.find((p) => p.name === promptName); if (!promptConfig) { console.error(`Prompt "${promptName}" not found. Available:`, prompts.map((p) => p.name).join(", ")); process.exit(1); } if (!shouldRun(promptConfig)) { console.log(`"${promptName}" skipped (too soon or random skip). Next time.`); process.exit(0); } const kernel = new Kernel({ apiKey: process.env.KERNEL }); const browser = await kernel.browsers.create({ stealth: true }); const sessionId = browser.session_id; console.log(`[${promptName}] Session: ${sessionId}`); try { const url = await login(kernel, sessionId); if (!url.includes("x.com/home")) { throw new Error("Login didn't reach home page: " + url); } const history = loadHistory(promptName); const rawTweet = await generateTweet(promptConfig, history); let tweet = rawTweet; if (promptConfig.links) { for (const [key, linkUrl] of Object.entries(promptConfig.links)) { tweet = tweet.replaceAll(`<${key}>`, linkUrl); } } console.log(`[${promptName}] Tweet: "${tweet}"`); await postTweet(kernel, sessionId, tweet); console.log(`[${promptName}] Posted!`); history.push({ tweet: rawTweet, date: new Date().toISOString() }); saveHistory(promptName, history); console.log(`[${promptName}] Total: ${history.length}`); } finally { try { await kernel.browsers.delete(sessionId); console.log("Session closed."); } catch {} } } main().catch((err) => { console.error("Failed:", err.message); process.exit(1); });