Consolidate schedule config, support multi-post per day

- Schedule config moved to nested schedule object in prompts.json
- Scheduler picks 1-3 random times for personality with 4hr min gap
- shouldRun reads from schedule object

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Jackson
2026-03-07 23:12:29 +00:00
parent 417f943239
commit d2ca2d23ba
3 changed files with 87 additions and 37 deletions

14
bot.js
View File

@@ -28,18 +28,16 @@ function saveHistory(name, history) {
}
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);
const now = new Date();
if (promptConfig.frequency === "daily") {
return true;
}
if (promptConfig.frequency === "random" && promptConfig.minDays) {
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 < promptConfig.minDays) return false;
if (daysSinceLast < schedule.minDays) return false;
}
// Random chance so it doesn't always fire on the minimum day
return Math.random() < 0.5;

View File

@@ -2,19 +2,32 @@
{
"name": "tease",
"prompt": "You are writing a tweet for a woman who is quietly building a creative portfolio and wants people to visit her site out of curiosity.\n\nTone: casual, understated, real. Like she is just talking \u2014 not performing, not selling, not trying too hard. No hashtags. No emojis unless they feel completely natural. Never use the word \"model\" or any fashion/influencer language.\n\nThe goal is to make someone curious enough to click the link. The best tweets hint at something without explaining it. They feel personal but not oversharing. Confident but not loud.\n\nGenerate 1 tweet. Keep it under 280 characters. Just the tweet, nothing else.\n\nEnd the tweet with <link> on its own line.",
"frequency": "random",
"startHour": 8,
"endHour": 20,
"minDays": 2,
"links": {
"link": "https://onlyfans.com/juniper_sky"
},
"schedule": {
"type": "random",
"window": [
8,
20
],
"minDays": 2
}
},
{
"name": "personality",
"prompt": "You are writing a tweet for a woman in her late 20s to late 30s living in the Pacific Northwest. She reads constantly, makes pottery, spends time in the woods, and has a declining interest in being perceived.\n\nTone: dry, witty, slightly feral, cozy. Dark humor, sarcastic observations, occasional leftist commentary. Never preachy. Never try-hard. Feels like something she muttered to herself and decided to post.\n\nNo hashtags. No emojis unless they earn it. No links. No advertising. Nothing aspirational or girlboss. Just something real that makes someone think \"oh that is me.\"\n\nGenerate 1 tweet. Just the tweet. Nothing else.",
"frequency": "daily",
"startHour": 8,
"endHour": 20
"schedule": {
"type": "daily",
"window": [
8,
20
],
"postsPerDay": [
1,
3
],
"minGapHours": 4
}
}
]

View File

@@ -19,33 +19,72 @@ if (!config) {
process.exit(1);
}
// Pick a random time in the window
const windowMinutes = (config.endHour - config.startHour) * 60;
const delayMinutes = Math.floor(Math.random() * windowMinutes);
const hours = Math.floor(delayMinutes / 60) + config.startHour;
const minutes = delayMinutes % 60;
const schedule = config.schedule;
const [startHour, endHour] = schedule.window;
const minGap = (schedule.minGapHours || 4) * 60;
const pad = (n) => String(n).padStart(2, "0");
console.log(`[${promptName}] Scheduled for ${pad(hours)}:${pad(minutes)} PST`);
const now = new Date(
new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles" })
);
const target = new Date(now);
target.setHours(hours, minutes, 0, 0);
let delayMs = target.getTime() - now.getTime();
if (delayMs <= 0) {
console.log("Target time passed. Posting now.");
delayMs = 0;
function randomMinuteInWindow() {
const windowMinutes = (endHour - startHour) * 60;
return Math.floor(Math.random() * windowMinutes);
}
console.log(`Waiting ${Math.round(delayMs / 60000)} minutes...`);
function pickSpacedTimes(count) {
for (let attempt = 0; attempt < 100; attempt++) {
const times = Array.from({ length: count }, () => randomMinuteInWindow()).sort((a, b) => a - b);
let valid = true;
for (let i = 1; i < times.length; i++) {
if (times[i] - times[i - 1] < minGap) {
valid = false;
break;
}
}
if (valid) return times;
}
const window = (endHour - startHour) * 60;
const gap = Math.floor(window / (count + 1));
return Array.from({ length: count }, (_, i) => gap * (i + 1));
}
setTimeout(() => {
function nowPST() {
return new Date(new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles" }));
}
function runBot() {
try {
execFileSync("node", [join(__dirname, "bot.js"), promptName], { stdio: "inherit" });
} catch {
process.exit(1);
console.error(`[${promptName}] bot.js failed`);
}
}, delayMs);
}
const [minPerDay, maxPerDay] = schedule.postsPerDay || [1, 1];
const count = Math.floor(Math.random() * (maxPerDay - minPerDay + 1)) + minPerDay;
const times = pickSpacedTimes(count);
const pad = (n) => String(n).padStart(2, "0");
const scheduled = times.map((m) => `${pad(Math.floor(m / 60) + startHour)}:${pad(m % 60)}`);
console.log(`[${promptName}] ${count} post(s) scheduled at: ${scheduled.join(", ")} PST`);
function scheduleNext(index) {
if (index >= times.length) return;
const minutes = times[index];
const hours = Math.floor(minutes / 60) + startHour;
const mins = minutes % 60;
const now = nowPST();
const target = new Date(now);
target.setHours(hours, mins, 0, 0);
let delayMs = target.getTime() - now.getTime();
if (delayMs < 0) delayMs = 0;
console.log(`[${promptName}] #${index + 1} in ${Math.round(delayMs / 60000)} minutes`);
setTimeout(() => {
runBot();
scheduleNext(index + 1);
}, delayMs);
}
scheduleNext(0);