Created
March 18, 2026 02:11
-
-
Save SebConejo/94a779aba89090d6bf81896df169c938 to your computer and use it in GitHub Desktop.
OpenClaw scripts for autonomous social media monitoring and posting
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * reddit-comment.mjs | |
| * Posts a comment on a Reddit thread using Puppeteer with your existing Chrome session. | |
| * | |
| * Usage: | |
| * node reddit-comment.mjs --url "https://www.reddit.com/r/openclaw/comments/..." --comment "Your comment here" | |
| * | |
| * Requirements: | |
| * npm install puppeteer-core | |
| * | |
| * This script connects to your existing Chrome profile (already logged into Reddit) | |
| * so no API keys or OAuth needed. | |
| */ | |
| import puppeteer from 'puppeteer-core'; | |
| import { execSync } from 'child_process'; | |
| import { parseArgs } from 'node:util'; | |
| // Parse command line arguments | |
| const { values } = parseArgs({ | |
| options: { | |
| url: { type: 'string' }, | |
| comment: { type: 'string' }, | |
| 'chrome-path': { type: 'string', default: '' }, | |
| 'dry-run': { type: 'boolean', default: false }, | |
| } | |
| }); | |
| if (!values.url || !values.comment) { | |
| console.error('ERROR: Missing required arguments'); | |
| console.error('Usage: node reddit-comment.mjs --url "https://reddit.com/r/.../comments/..." --comment "Your comment"'); | |
| process.exit(1); | |
| } | |
| const POST_URL = values.url; | |
| const COMMENT_TEXT = values.comment; | |
| const DRY_RUN = values['dry-run']; | |
| // Find Chrome on macOS | |
| function findChrome() { | |
| if (values['chrome-path']) return values['chrome-path']; | |
| const paths = [ | |
| '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', | |
| '/Applications/Chromium.app/Contents/MacOS/Chromium', | |
| '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser', | |
| ]; | |
| for (const p of paths) { | |
| try { | |
| execSync(`test -f "${p}"`); | |
| return p; | |
| } catch { continue; } | |
| } | |
| console.error('ERROR: Chrome not found. Use --chrome-path to specify location.'); | |
| process.exit(1); | |
| } | |
| // Get Chrome user data directory | |
| function getChromeUserDataDir() { | |
| return `${process.env.HOME}/Library/Application Support/Google/Chrome`; | |
| } | |
| async function main() { | |
| const chromePath = findChrome(); | |
| const userDataDir = getChromeUserDataDir(); | |
| console.log(`[1/6] Launching Chrome with existing profile...`); | |
| const browser = await puppeteer.launch({ | |
| executablePath: chromePath, | |
| userDataDir: userDataDir, | |
| headless: false, // Reddit detects headless | |
| args: [ | |
| '--no-first-run', | |
| '--disable-blink-features=AutomationControlled', | |
| '--disable-infobars', | |
| ], | |
| defaultViewport: { width: 1280, height: 900 }, | |
| }); | |
| const page = await browser.newPage(); | |
| // Set a realistic user agent | |
| await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'); | |
| try { | |
| // Step 2: Navigate to the post | |
| console.log(`[2/6] Navigating to post: ${POST_URL}`); | |
| await page.goto(POST_URL, { waitUntil: 'networkidle2', timeout: 30000 }); | |
| await sleep(2000); | |
| // Step 3: Verify we're logged in | |
| console.log(`[3/6] Checking login status...`); | |
| const loginCheck = await page.evaluate(() => { | |
| // Check for logged-in indicators | |
| const userMenu = document.querySelector('[id*="USER"]') || | |
| document.querySelector('button[aria-label*="profile"]') || | |
| document.querySelector('a[href*="/user/"]'); | |
| return !!userMenu; | |
| }); | |
| if (!loginCheck) { | |
| console.error('ERROR: Not logged into Reddit. Please log in manually on Chrome first.'); | |
| await browser.close(); | |
| process.exit(2); | |
| } | |
| console.log(`[3/6] ✓ Logged in`); | |
| // Step 4: Find the comment box | |
| console.log(`[4/6] Finding comment box...`); | |
| // Reddit's new UI uses a contenteditable div or textarea | |
| // Try multiple selectors for compatibility | |
| const commentBoxSelector = await page.evaluate(() => { | |
| // New Reddit (shreddit) | |
| const shredditBox = document.querySelector('shreddit-composer') || | |
| document.querySelector('[name="body"]') || | |
| document.querySelector('div[contenteditable="true"][data-lexical-editor]') || | |
| document.querySelector('div[role="textbox"]') || | |
| document.querySelector('.public-DraftEditor-content') || | |
| document.querySelector('textarea[placeholder*="comment"]') || | |
| document.querySelector('textarea[placeholder*="thought"]'); | |
| if (shredditBox) return 'found'; | |
| return null; | |
| }); | |
| if (!commentBoxSelector) { | |
| console.error('ERROR: Could not find comment box. The post might not allow comments.'); | |
| await browser.close(); | |
| process.exit(3); | |
| } | |
| // Click on the comment area to activate it | |
| const commentArea = await page.$('div[contenteditable="true"]') || | |
| await page.$('div[role="textbox"]') || | |
| await page.$('textarea[placeholder*="comment"]') || | |
| await page.$('textarea[placeholder*="thought"]') || | |
| await page.$('.public-DraftEditor-content'); | |
| if (!commentArea) { | |
| // Try clicking on the comment prompt text to activate the editor | |
| const promptTexts = await page.$('div[data-click-id="text"]'); | |
| if (promptTexts) { | |
| await promptTexts.click(); | |
| await sleep(1000); | |
| } | |
| } else { | |
| await commentArea.click(); | |
| await sleep(500); | |
| } | |
| // Step 5: Type the comment | |
| console.log(`[5/6] Typing comment (${COMMENT_TEXT.length} chars)...`); | |
| if (DRY_RUN) { | |
| console.log(`[DRY RUN] Would post: "${COMMENT_TEXT.substring(0, 100)}..."`); | |
| console.log(`[DRY RUN] Skipping actual submission.`); | |
| await browser.close(); | |
| process.exit(0); | |
| } | |
| // Type with realistic delays | |
| const activeElement = await page.$('div[contenteditable="true"]:focus') || | |
| await page.$('div[role="textbox"]') || | |
| await page.$('textarea:focus'); | |
| if (activeElement) { | |
| await activeElement.type(COMMENT_TEXT, { delay: 30 }); | |
| } else { | |
| // Fallback: use keyboard directly | |
| await page.keyboard.type(COMMENT_TEXT, { delay: 30 }); | |
| } | |
| await sleep(1000); | |
| // Step 6: Click the submit button | |
| console.log(`[6/6] Submitting comment...`); | |
| // Find and click the Comment/Reply button | |
| const submitButton = await page.evaluateHandle(() => { | |
| const buttons = Array.from(document.querySelectorAll('button')); | |
| const commentBtn = buttons.find(b => { | |
| const text = b.textContent.trim().toLowerCase(); | |
| return (text === 'comment' || text === 'reply') && !b.disabled; | |
| }); | |
| return commentBtn; | |
| }); | |
| if (!submitButton || !(await submitButton.asElement())) { | |
| console.error('ERROR: Could not find the Comment/Reply button. It might be disabled.'); | |
| // Take a screenshot for debugging | |
| await page.screenshot({ path: '/tmp/reddit-comment-debug.png' }); | |
| console.error('DEBUG: Screenshot saved to /tmp/reddit-comment-debug.png'); | |
| await browser.close(); | |
| process.exit(4); | |
| } | |
| await submitButton.asElement().click(); | |
| await sleep(3000); | |
| // Verify the comment was posted | |
| console.log(`[VERIFY] Checking if comment appears on page...`); | |
| const commentPosted = await page.evaluate((text) => { | |
| const bodyText = document.body.innerText; | |
| // Check if our comment text appears on the page | |
| return bodyText.includes(text.substring(0, 50)); | |
| }, COMMENT_TEXT); | |
| if (commentPosted) { | |
| // Get the current URL (it might have changed to include the comment) | |
| const currentUrl = page.url(); | |
| console.log(`\nSUCCESS: Comment posted!`); | |
| console.log(`URL: ${currentUrl}`); | |
| console.log(`PROOF: Comment text found on page after submission.`); | |
| console.log(`COMMENT: "${COMMENT_TEXT.substring(0, 100)}${COMMENT_TEXT.length > 100 ? '...' : ''}"`); | |
| } else { | |
| console.error(`\nWARNING: Comment may not have been posted. Text not found on page after submission.`); | |
| await page.screenshot({ path: '/tmp/reddit-comment-result.png' }); | |
| console.error('DEBUG: Screenshot saved to /tmp/reddit-comment-result.png'); | |
| } | |
| await browser.close(); | |
| } catch (error) { | |
| console.error(`\nFATAL ERROR: ${error.message}`); | |
| try { | |
| await page.screenshot({ path: '/tmp/reddit-comment-error.png' }); | |
| console.error('DEBUG: Error screenshot saved to /tmp/reddit-comment-error.png'); | |
| } catch {} | |
| await browser.close(); | |
| process.exit(5); | |
| } | |
| } | |
| function sleep(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| main(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * reddit-search.mjs | |
| * Searches Reddit for high-potential posts to comment on. | |
| * Uses Reddit's public JSON API (no auth needed for reading). | |
| * | |
| * Usage: | |
| * node reddit-search.mjs --subreddit "openclaw" --sort "hot" --limit 10 | |
| * node reddit-search.mjs --subreddit "openclaw,LocalLLM,ClawdBot" --sort "new" --min-score 5 | |
| * node reddit-search.mjs --search "LLM cost optimization" --sort "relevance" | |
| * | |
| * Output: JSON array of posts with metadata for the agent to evaluate. | |
| */ | |
| import { parseArgs } from 'node:util'; | |
| const { values } = parseArgs({ | |
| options: { | |
| subreddit: { type: 'string', default: 'openclaw' }, | |
| search: { type: 'string', default: '' }, | |
| sort: { type: 'string', default: 'hot' }, // hot, new, top, rising | |
| limit: { type: 'string', default: '15' }, | |
| 'min-score': { type: 'string', default: '0' }, | |
| 'max-comments': { type: 'string', default: '999' }, // posts with fewer comments = more opportunity | |
| 'max-age-hours': { type: 'string', default: '48' }, | |
| } | |
| }); | |
| const SUBREDDITS = values.subreddit.split(',').map(s => s.trim()); | |
| const SEARCH_QUERY = values.search; | |
| const SORT = values.sort; | |
| const LIMIT = parseInt(values.limit); | |
| const MIN_SCORE = parseInt(values['min-score']); | |
| const MAX_COMMENTS = parseInt(values['max-comments']); | |
| const MAX_AGE_HOURS = parseInt(values['max-age-hours']); | |
| const HEADERS = { | |
| 'User-Agent': 'Mozilla/5.0 (compatible; script/1.0)', | |
| 'Accept': 'application/json', | |
| }; | |
| async function fetchSubreddit(subreddit, sort, limit) { | |
| const url = `https://www.reddit.com/r/${subreddit}/${sort}.json?limit=${limit}&raw_json=1`; | |
| const response = await fetch(url, { headers: HEADERS }); | |
| if (!response.ok) { | |
| console.error(`ERROR: Failed to fetch r/${subreddit}: ${response.status}`); | |
| return []; | |
| } | |
| const data = await response.json(); | |
| return data.data.children.map(child => child.data); | |
| } | |
| async function searchReddit(query, sort, limit) { | |
| const url = `https://www.reddit.com/search.json?q=${encodeURIComponent(query)}&sort=${sort}&limit=${limit}&raw_json=1`; | |
| const response = await fetch(url, { headers: HEADERS }); | |
| if (!response.ok) { | |
| console.error(`ERROR: Search failed: ${response.status}`); | |
| return []; | |
| } | |
| const data = await response.json(); | |
| return data.data.children.map(child => child.data); | |
| } | |
| function filterPosts(posts) { | |
| const now = Date.now() / 1000; | |
| const maxAge = MAX_AGE_HOURS * 3600; | |
| return posts.filter(post => { | |
| const age = now - post.created_utc; | |
| if (age > maxAge) return false; | |
| if (post.score < MIN_SCORE) return false; | |
| if (post.num_comments > MAX_COMMENTS) return false; | |
| if (post.locked || post.archived) return false; | |
| if (post.over_18) return false; | |
| return true; | |
| }); | |
| } | |
| function scorePosts(posts) { | |
| return posts.map(post => { | |
| const ageHours = (Date.now() / 1000 - post.created_utc) / 3600; | |
| // Scoring: higher = better opportunity | |
| let opportunityScore = 0; | |
| // High upvotes = trending | |
| if (post.score > 100) opportunityScore += 3; | |
| else if (post.score > 30) opportunityScore += 2; | |
| else if (post.score > 5) opportunityScore += 1; | |
| // Few comments = room to be seen | |
| if (post.num_comments < 5) opportunityScore += 3; | |
| else if (post.num_comments < 15) opportunityScore += 2; | |
| else if (post.num_comments < 30) opportunityScore += 1; | |
| // Fresh = better | |
| if (ageHours < 4) opportunityScore += 3; | |
| else if (ageHours < 12) opportunityScore += 2; | |
| else if (ageHours < 24) opportunityScore += 1; | |
| // Upvote velocity (score per hour) | |
| const velocity = ageHours > 0 ? post.score / ageHours : 0; | |
| if (velocity > 10) opportunityScore += 2; | |
| else if (velocity > 3) opportunityScore += 1; | |
| // Topic relevance keywords | |
| const titleLower = (post.title + ' ' + (post.selftext || '')).toLowerCase(); | |
| const costKeywords = ['cost', 'expensive', 'bill', 'spend', 'token', 'pricing', 'budget', 'save', 'cheap']; | |
| const routingKeywords = ['routing', 'model', 'haiku', 'opus', 'sonnet', 'switch', 'fallback']; | |
| const setupKeywords = ['setup', 'install', 'configure', 'beginner', 'started', 'first']; | |
| if (costKeywords.some(k => titleLower.includes(k))) opportunityScore += 2; | |
| if (routingKeywords.some(k => titleLower.includes(k))) opportunityScore += 2; | |
| if (setupKeywords.some(k => titleLower.includes(k))) opportunityScore += 1; | |
| return { | |
| ...post, | |
| opportunityScore, | |
| ageHours: Math.round(ageHours * 10) / 10, | |
| velocity: Math.round(velocity * 10) / 10, | |
| }; | |
| }).sort((a, b) => b.opportunityScore - a.opportunityScore); | |
| } | |
| async function main() { | |
| let allPosts = []; | |
| if (SEARCH_QUERY) { | |
| console.error(`Searching Reddit for: "${SEARCH_QUERY}"...`); | |
| const posts = await searchReddit(SEARCH_QUERY, SORT, LIMIT); | |
| allPosts = allPosts.concat(posts); | |
| } else { | |
| for (const sub of SUBREDDITS) { | |
| console.error(`Fetching r/${sub} (${SORT})...`); | |
| const posts = await fetchSubreddit(sub, SORT, LIMIT); | |
| allPosts = allPosts.concat(posts); | |
| // Rate limit: wait between requests | |
| await new Promise(r => setTimeout(r, 1000)); | |
| } | |
| } | |
| console.error(`Fetched ${allPosts.length} posts total.`); | |
| const filtered = filterPosts(allPosts); | |
| console.error(`${filtered.length} posts after filtering.`); | |
| const scored = scorePosts(filtered); | |
| // Output clean JSON for the agent | |
| const output = scored.slice(0, LIMIT).map(post => ({ | |
| title: post.title, | |
| subreddit: post.subreddit, | |
| url: `https://www.reddit.com${post.permalink}`, | |
| score: post.score, | |
| comments: post.num_comments, | |
| ageHours: post.ageHours, | |
| velocity: post.velocity, | |
| opportunityScore: post.opportunityScore, | |
| flair: post.link_flair_text || null, | |
| author: post.author, | |
| preview: (post.selftext || '').substring(0, 200), | |
| })); | |
| // Output to stdout (for the agent to parse) | |
| console.log(JSON.stringify(output, null, 2)); | |
| } | |
| main().catch(err => { | |
| console.error(`FATAL: ${err.message}`); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment