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"); const DATA_DIR = process.env.DATA_DIR || __dirname; function loadPrompts() { return JSON.parse(readFileSync(PROMPTS_FILE, "utf-8")); } function historyPath(name) { return join(DATA_DIR, `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 schedule = promptConfig.schedule; if (!schedule || schedule.type === "daily") return true; if (schedule.type === "random" && schedule.minDays) { const history = loadHistory(promptConfig.name); if (history.length > 0) { const now = new Date(); const lastPost = new Date(history[history.length - 1].date); const daysSinceLast = (now - lastPost) / (1000 * 60 * 60 * 24); if (daysSinceLast < schedule.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, account) { const prefix = account.toUpperCase(); const xAcct = JSON.stringify(process.env[`${prefix}_USER`]); const xPw = JSON.stringify(process.env[`${prefix}_PW`]); const xEmail = JSON.stringify(process.env[`${prefix}_EMAIL`]); const xPhone = JSON.stringify(process.env[`${prefix}_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 page.waitForTimeout(300); await page.keyboard.type(${xAcct}, { delay: 50 }); 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); // Adaptive login flow — keep reading the page and responding for (let step = 0; step < 5; step++) { const pageState = await readPage(kernel, sessionId); const pageText = pageState.texts.join(" ").toLowerCase(); const inputTypes = pageState.inputs.map((i) => i.type); console.log(`Step ${step}: url=${pageState.url}, inputs=[${inputTypes}], text=${pageText.slice(0, 200)}`); // Password field visible — we're ready to enter password if (inputTypes.includes("password")) { console.log("Password field found"); break; } // Determine what to type based on what the page is asking let value; if (pageText.includes("phone") && pageText.includes("email")) { value = xPhone; console.log("Page asks for phone or email — using phone"); } else if (pageText.includes("phone")) { value = xPhone; console.log("Page asks for phone"); } else if (pageText.includes("email")) { value = xEmail; console.log("Page asks for email"); } else if (pageText.includes("username")) { value = xAcct; console.log("Page asks for username"); } else { console.log("Unknown page state, trying phone"); value = xPhone; } const rv = await exec(kernel, sessionId, ` const inputs = await page.locator('input:visible').all(); const input = inputs[inputs.length - 1]; await input.click(); await input.clear(); await page.waitForTimeout(300); await page.keyboard.type(${value}, { delay: 50 }); await page.waitForTimeout(500); const nextBtn = page.locator('[role="button"]').filter({ hasText: /Next|Log in|Continue/i }); if (await nextBtn.count() > 0) { await nextBtn.first().click(); } await page.waitForTimeout(5000); `, 30); if (!rv.success) throw new Error(`Step ${step} failed: ` + rv.error); } const finalState = await readPage(kernel, sessionId); if (!finalState.inputs.some((i) => i.type === "password")) { console.log("Final state:", JSON.stringify(finalState, null, 2)); throw new Error("Never reached password field"); } const r2 = await exec(kernel, sessionId, ` const input = page.locator('input[type="password"]'); await input.waitFor({ state: 'visible', timeout: 10000 }); await input.click(); await page.waitForTimeout(500); await page.keyboard.type(${xPw}, { delay: 50 }); await page.waitForTimeout(500); const loginBtn = page.getByTestId('LoginForm_Login_Button'); await loginBtn.click(); await page.waitForTimeout(8000); return page.url(); `, 45); if (!r2.success) throw new Error("Password failed: " + r2.error); console.log("Logged in:", r2.result); return r2.result; } async function postTweet(kernel, sessionId, tweetText) { const composeResult = await exec(kernel, sessionId, ` await page.goto('https://x.com/compose/post', { waitUntil: 'domcontentloaded', timeout: 15000 }); await page.waitForTimeout(3000); 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 }); // Clean up any stale sessions try { const sessions = await kernel.browsers.list(); if (sessions.length > 0) { console.log(`Cleaning up ${sessions.length} stale session(s)...`); for (const s of sessions) { try { await kernel.browsers.delete(s.session_id); } catch {} } } } catch {} 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, promptConfig.account); if (!url.includes("x.com")) { throw new Error("Login failed: " + 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); });