/** * ๐Ÿ›๏ธ Pi-Nanny Extension - Parental Control for Late Night Coding * * Helps you maintain a healthy sleep schedule by nagging you to go to bed * when you're having "47 creative new ideas at 3 am instead of sleeping". * * Features: * - Configurable bedtime hours (default: midnight to 6 AM) * - Progressive enforcement: warnings โ†’ annoyances โ†’ blockades * - Snooze option for "just one more thing" * - Persistent status indicator showing time until you should be in bed * * Commands: * - /nanny - Check bedtime status * - /nanny snooze - Temporary override (default: 15 min) * - /nanny config - View/update bedtime hours * - /nanny disable - Turn off for this session * * Usage: * pi -e ./pi-nanny.ts * * Or install globally: * mkdir -p ~/.pi/agent/extensions * cp pi-nanny.ts ~/.pi/agent/extensions/ */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; interface NannyConfig { enabled: boolean; bedtimeStart: number; // Hour (0-23) when bedtime starts bedtimeEnd: number; // Hour (0-23) when bedtime ends hardExitHour: number; // Hour (0-23) when Pi force-quits strictMode: boolean; // If true, blocks input during bedtime snoozeUntil?: number; // Timestamp when snooze expires warningCount: number; // Track how many times user ignored warnings hasShownHardExitWarning: boolean; // Don't spam warnings // TEST MODE testMode: boolean; // Enable time simulation for testing testHour?: number; // Simulated hour (0-23) } const DEFAULT_CONFIG: NannyConfig = { enabled: true, bedtimeStart: 0, // Midnight bedtimeEnd: 6, // 6 AM hardExitHour: 1, // 1 AM - absolute cutoff strictMode: false, // Start gentle snoozeUntil: undefined, warningCount: 0, hasShownHardExitWarning: false, testMode: false, testHour: undefined, }; export default function (pi: ExtensionAPI) { let config: NannyConfig = { ...DEFAULT_CONFIG }; /** * Get current hour (or test hour if in test mode) */ function getCurrentHour(): number { if (config.testMode && config.testHour !== undefined) { return config.testHour; } return new Date().getHours(); } /** * Check if current time is within bedtime hours */ function isBedtime(): boolean { if (!config.enabled) return false; if (config.snoozeUntil && Date.now() < config.snoozeUntil) return false; const hour = getCurrentHour(); // Handle overnight range (e.g., 0-6 or 23-7) if (config.bedtimeStart > config.bedtimeEnd) { return hour >= config.bedtimeStart || hour < config.bedtimeEnd; } return hour >= config.bedtimeStart && hour < config.bedtimeEnd; } /** * Check if we're past the hard exit time (no snooze allowed) */ function isPastHardExit(): boolean { if (!config.enabled) return false; const hour = getCurrentHour(); // Must be in bedtime to trigger hard exit if (!isBedtime()) return false; // For overnight bedtime (0-6), hard exit at 1 means: hour >= 1 && hour < 6 if (config.bedtimeStart > config.bedtimeEnd) { // We're in bedtime, so check if hour >= hardExitHour // If hard exit is 1 and we're at 2 AM, we're past it return hour >= config.hardExitHour && hour < config.bedtimeEnd; } // Normal range (not overnight) return hour >= config.hardExitHour && hour < config.bedtimeEnd; } /** * Get a random funny exit message for forced shutdown */ function getExitMessage(): string { const messages = [ "Good night sucker! ๐Ÿ˜ด", "That's it. I'm pulling the plug. Sweet dreams! ๐Ÿ”Œ", "Your 3 AM ideas can wait. Trust me. ๐ŸŒ™", "Shutting down before you do something you'll regret. ๐Ÿ’ค", "I'm not mad, just disappointed. And also shutting down. ๐Ÿ˜ค", "The bugs will still be there tomorrow. I promise. ๐Ÿ›", "Consider this an intervention. *yanks power cord* ๐Ÿ”Œ", "Git commit yourself to bed. NOW. ๐Ÿ›๏ธ", "Stackoverflow will still be there in the morning. Bye! ๐Ÿ‘‹", "Your IDE is going to sleep. You should too. ๐Ÿ’ป๐Ÿ’ค", "Emergency override: SLEEP > CODE. Goodnight! ๐Ÿšจ", "I'm doing this FOR you, not TO you. Sleep well! ๐Ÿ˜‡", "Time to deploy yourself to production... er, bed. ๐Ÿš€๐Ÿ›๏ธ", "You have been forcefully logged out of consciousness. ๐Ÿ˜ด", "Bedtime.exe has stopped responding. Forcing shutdown. ๐Ÿ’ค", "Your mom called. She said go to bed. (jk I'm pulling the plug) ๐Ÿ“ž", "The rubber duck debugging can wait. Quack quack, good night! ๐Ÿฆ†", "Compiling dreams... 100% complete. Shutting down. โœจ", "404: Wakefulness not found. Redirecting to sleep. ๐ŸŒ™", "Ctrl+Alt+Delete your bad sleep habits. Good night! โŒจ๏ธ", ]; return messages[Math.floor(Math.random() * messages.length)]; } /** * Get a nagging message based on severity */ function getNagMessage(severity: "gentle" | "firm" | "strict"): string { const messages = { gentle: [ "๐ŸŒ™ It's late. Maybe consider wrapping up soon?", "๐Ÿ’ค Your bed misses you.", "๐Ÿ›๏ธ Remember: bugs will still be there tomorrow.", "๐Ÿ˜ด Even GPUs need to sleep sometimes.", "๐ŸŒƒ The code will wait. Your sleep schedule won't.", ], firm: [ "๐Ÿšจ Seriously, it's bedtime. This isn't healthy.", "โš ๏ธ Your future self will thank you for sleeping now.", "๐Ÿ›‘ Stop. Go to bed. This is not a suggestion anymore.", "๐Ÿ˜ค I'm starting to get concerned about you.", "๐Ÿ’€ You're entering zombie developer territory.", ], strict: [ "๐Ÿ”ด BEDTIME VIOLATION DETECTED. SHUTTING DOWN CREATIVITY MODULE.", "โŒ Access denied. Sleep > Code. No exceptions.", "๐Ÿšจ Emergency protocol activated. BED. NOW.", "โ›” This is an intervention. Close the laptop.", "๐Ÿ›๏ธ I will keep blocking you until you sleep. I'm not joking.", ], }; const pool = messages[severity]; return pool[Math.floor(Math.random() * pool.length)]; } /** * Format time remaining until bedtime ends */ function getTimeUntilMorning(): string { const hour = getCurrentHour(); const hoursUntil = config.bedtimeEnd > hour ? config.bedtimeEnd - hour : 24 - hour + config.bedtimeEnd; if (hoursUntil === 1) return "1 hour"; return `${hoursUntil} hours`; } /** * Update status line */ function updateStatus(ctx: any): void { if (!config.enabled) { ctx.ui.setStatus("pi-nanny", undefined); return; } let statusText = ""; if (config.testMode && config.testHour !== undefined) { statusText = `๐Ÿงช TEST (${config.testHour}:00) `; } // Hard exit warning takes priority if (isPastHardExit()) { ctx.ui.setStatus("pi-nanny", statusText + `๐Ÿšจ HARD EXIT - Shutting down soon!`); return; } if (config.snoozeUntil && Date.now() < config.snoozeUntil) { const remaining = Math.ceil((config.snoozeUntil - Date.now()) / 60000); const hour = getCurrentHour(); const untilHardExit = config.hardExitHour > hour ? config.hardExitHour - hour : 24 - hour + config.hardExitHour; if (untilHardExit <= 1) { ctx.ui.setStatus("pi-nanny", statusText + `๐Ÿ˜ด Snooze: ${remaining}m (โš ๏ธ hard exit at ${config.hardExitHour}:00)`); } else { ctx.ui.setStatus("pi-nanny", statusText + `๐Ÿ˜ด Snooze: ${remaining}m remaining`); } return; } if (isBedtime()) { const hour = getCurrentHour(); const now = new Date(); const minutesUntilHardExit = (config.hardExitHour - hour) * 60 - now.getMinutes(); if (minutesUntilHardExit > 0 && minutesUntilHardExit <= 60 && !config.testMode) { ctx.ui.setStatus("pi-nanny", statusText + `๐Ÿšจ Bedtime! Hard exit in ${minutesUntilHardExit}m`); } else { const emoji = config.strictMode ? "๐Ÿšจ" : "๐ŸŒ™"; ctx.ui.setStatus("pi-nanny", statusText + `${emoji} Bedtime! (ends in ${getTimeUntilMorning()})`); } } else { ctx.ui.setStatus("pi-nanny", statusText || undefined); } } /** * Handle hard exit with countdown */ async function handleHardExit(ctx: any): Promise { const exitMessage = getExitMessage(); // Show warning with countdown const proceed = await ctx.ui.confirm( "๐Ÿšจ MANDATORY BEDTIME", `It's past ${config.hardExitHour}:00 AM. Pi is shutting down in 30 seconds.\n\n${exitMessage}\n\nNo snooze available. Get some sleep!`, { timeout: 30000 }, ); // They tried to cancel but we don't care if (!proceed) { ctx.ui.notify(exitMessage, "error"); setTimeout(() => ctx.shutdown(), 2000); } else { // They clicked "Yes" but we're still shutting down ctx.ui.notify(`Nice try. ${exitMessage}`, "error"); setTimeout(() => ctx.shutdown(), 2000); } } // Session start: Show warning if it's bedtime pi.on("session_start", async (_event, ctx) => { // Check for test flag const testTime = pi.getFlag("--nanny-test-time"); if (testTime !== undefined) { config.testMode = true; config.testHour = parseInt(String(testTime)); ctx.ui.notify(`๐Ÿงช TEST MODE: Simulating hour ${config.testHour}:00`, "info"); } updateStatus(ctx); // Check for hard exit first if (isPastHardExit()) { await handleHardExit(ctx); return; } if (isBedtime()) { const message = config.strictMode ? "Starting Pi during bedtime is strongly discouraged. Use `/nanny snooze` if you really must." : "Starting Pi late at night? Don't stay up too long! ๐ŸŒ™"; ctx.ui.notify(message, "warning"); } }); // Agent start: Increment warning counter and check for hard exit pi.on("agent_start", async (_event, ctx) => { // Hard exit check if (isPastHardExit()) { if (!config.hasShownHardExitWarning) { config.hasShownHardExitWarning = true; await handleHardExit(ctx); } return; } if (isBedtime() && !config.snoozeUntil) { config.warningCount++; // Escalate to strict mode after 5 warnings if (config.warningCount >= 5 && !config.strictMode) { config.strictMode = true; ctx.ui.notify("๐Ÿšจ Strict mode activated. You've ignored too many warnings.", "error"); updateStatus(ctx); } } updateStatus(ctx); }); // Agent end: Remind after each response pi.on("agent_end", async (_event, ctx) => { // Update status after response updateStatus(ctx); // Hard exit check if (isPastHardExit()) { ctx.ui.notify("๐Ÿšจ It's really time to sleep now. Shutting down...", "error"); if (!config.hasShownHardExitWarning) { config.hasShownHardExitWarning = true; setTimeout(() => handleHardExit(ctx), 2000); } return; } // Gentle reminder after each response during bedtime if (isBedtime() && !config.snoozeUntil) { // Only nag every 2-3 responses to avoid being too annoying if (config.warningCount % 2 === 0) { const severity = config.strictMode ? "strict" : config.warningCount < 3 ? "gentle" : "firm"; const message = getNagMessage(severity); // Show as a subtle notification, not intrusive ctx.ui.notify(message, "warning"); } } }); // Input interception: Block or warn during bedtime pi.on("input", async (event, ctx) => { if (event.source === "extension") { return { action: "continue" }; } // Hard exit: no mercy, no snooze if (isPastHardExit()) { ctx.ui.notify(`๐Ÿšจ It's past ${config.hardExitHour}:00 AM. No more coding. Shutting down...`, "error"); if (!config.hasShownHardExitWarning) { config.hasShownHardExitWarning = true; setTimeout(() => handleHardExit(ctx), 1000); } return { action: "handled" }; } if (!isBedtime()) { return { action: "continue" }; } // Strict mode: block with confirmation dialog if (config.strictMode) { const proceed = await ctx.ui.confirm( "๐Ÿ›๏ธ Bedtime Enforcement", getNagMessage("strict") + "\n\nDo you REALLY need to continue? (Consider /nanny snooze)", { timeout: 10000 }, ); if (!proceed) { ctx.ui.notify("Good choice. Sweet dreams! ๐ŸŒ™", "info"); return { action: "handled" }; } // They insisted... add even more guilt ctx.ui.notify("๐Ÿ˜” Fine. But I'm judging you.", "warning"); return { action: "continue" }; } // Warning mode: just nag const severity = config.warningCount < 3 ? "gentle" : "firm"; ctx.ui.notify(getNagMessage(severity), "warning"); return { action: "continue" }; }); // Command: /nanny pi.registerCommand("nanny", { description: "Check bedtime status or configure Pi Nanny", handler: async (args, ctx) => { if (!args) { // Show status const now = new Date(); const currentHour = getCurrentHour(); const status = [ "๐Ÿ›๏ธ Pi-Nanny Status:", ` Enabled: ${config.enabled ? "Yes" : "No"}`, ` Bedtime: ${config.bedtimeStart}:00 - ${config.bedtimeEnd}:00`, ` Hard exit: ${config.hardExitHour}:00 AM (forced shutdown)`, config.testMode ? ` ๐Ÿงช TEST MODE: Simulating ${currentHour}:00` : ` Current time: ${now.getHours()}:${String(now.getMinutes()).padStart(2, "0")}`, ` In bedtime: ${isBedtime() ? "Yes ๐Ÿ˜ด" : "No โœ…"}`, ` Past hard exit: ${isPastHardExit() ? "Yes ๐Ÿšจ (shutting down soon!)" : "No"}`, ` Strict mode: ${config.strictMode ? "Yes ๐Ÿšจ" : "No"}`, ` Warnings ignored: ${config.warningCount}`, ]; if (config.snoozeUntil && Date.now() < config.snoozeUntil) { const remaining = Math.ceil((config.snoozeUntil - Date.now()) / 60000); status.push(` Snoozed: ${remaining} minutes remaining`); } status.push(""); status.push("Commands:"); status.push(" /nanny test [bedtime|hardexit|normal] - Test mode simulation"); status.push(" /nanny snooze [minutes] - Snooze for N minutes (default: 15)"); status.push(" /nanny config - Configure bedtime hours"); status.push(" /nanny disable - Disable for this session"); status.push(" /nanny enable - Re-enable"); status.push(" /nanny reset - Reset warning counter"); for (const line of status) { ctx.ui.notify(line, "info"); } return; } const [command, ...commandArgs] = args.trim().split(/\s+/); // TEST COMMANDS if (command === "test") { const testScenario = commandArgs[0]; if (!testScenario) { ctx.ui.notify("Usage: /nanny test [bedtime|hardexit|normal]", "warning"); ctx.ui.notify(" bedtime - Simulate midnight (warnings)", "info"); ctx.ui.notify(" hardexit - Simulate 1 AM (forced shutdown)", "info"); ctx.ui.notify(" normal - Switch back to real time", "info"); return; } if (testScenario === "bedtime") { config.testMode = true; config.testHour = 0; // Midnight config.hasShownHardExitWarning = false; ctx.ui.notify("๐Ÿงช TEST MODE: Simulating midnight (bedtime start)", "info"); ctx.ui.notify("Try sending prompts to see warnings!", "info"); updateStatus(ctx); return; } if (testScenario === "hardexit") { config.testMode = true; config.testHour = 1; // 1 AM config.hasShownHardExitWarning = false; ctx.ui.notify("๐Ÿงช TEST MODE: Simulating 1 AM (hard exit time)", "warning"); ctx.ui.notify("โš ๏ธ WARNING: This will trigger shutdown on next input!", "error"); updateStatus(ctx); return; } if (testScenario === "normal") { config.testMode = false; config.testHour = undefined; config.hasShownHardExitWarning = false; ctx.ui.notify("๐Ÿงช TEST MODE: Switched back to real time", "info"); updateStatus(ctx); return; } ctx.ui.notify(`Unknown test scenario: ${testScenario}`, "error"); ctx.ui.notify("Available: bedtime, hardexit, normal", "info"); return; } if (command === "snooze") { // Can't snooze past hard exit if (isPastHardExit()) { ctx.ui.notify(`๐Ÿšจ Too late! It's past ${config.hardExitHour}:00 AM. No snooze allowed!`, "error"); return; } const minutes = parseInt(commandArgs[0]) || 15; const hour = getCurrentHour(); const now = new Date(); const snoozeEnd = new Date(Date.now() + minutes * 60000); // Check if snooze would go past hard exit (skip check in test mode) if (!config.testMode && snoozeEnd.getHours() >= config.hardExitHour && hour < config.hardExitHour) { const maxMinutes = (config.hardExitHour - hour) * 60 - now.getMinutes(); ctx.ui.notify( `โš ๏ธ Can't snooze past ${config.hardExitHour}:00 AM (hard exit). Max snooze: ${maxMinutes} minutes`, "warning", ); return; } config.snoozeUntil = Date.now() + minutes * 60000; config.warningCount = Math.max(0, config.warningCount - 2); // Reset some guilt ctx.ui.notify(`๐Ÿ˜ด Snoozed for ${minutes} minutes. Make it count!`, "info"); updateStatus(ctx); return; } if (command === "config") { const startStr = await ctx.ui.input("Bedtime start hour (0-23):", String(config.bedtimeStart)); const endStr = await ctx.ui.input("Bedtime end hour (0-23):", String(config.bedtimeEnd)); const hardExitStr = await ctx.ui.input("Hard exit hour (0-23):", String(config.hardExitHour)); if (startStr) { const start = parseInt(startStr); if (start >= 0 && start <= 23) config.bedtimeStart = start; } if (endStr) { const end = parseInt(endStr); if (end >= 0 && end <= 23) config.bedtimeEnd = end; } if (hardExitStr) { const hardExit = parseInt(hardExitStr); if (hardExit >= 0 && hardExit <= 23) { config.hardExitHour = hardExit; config.hasShownHardExitWarning = false; // Reset warning flag } } ctx.ui.notify( `๐Ÿ›๏ธ Configured:\n Bedtime: ${config.bedtimeStart}:00 - ${config.bedtimeEnd}:00\n Hard exit: ${config.hardExitHour}:00`, "info", ); updateStatus(ctx); return; } if (command === "disable") { config.enabled = false; ctx.ui.notify("Pi-Nanny disabled. You're on your own now. ๐Ÿซก", "warning"); updateStatus(ctx); return; } if (command === "enable") { config.enabled = true; config.warningCount = 0; ctx.ui.notify("Pi-Nanny re-enabled. Welcome back to responsible coding! ๐Ÿ›๏ธ", "info"); updateStatus(ctx); return; } if (command === "reset") { config.warningCount = 0; config.strictMode = false; config.snoozeUntil = undefined; ctx.ui.notify("Warning counter reset. Fresh start! โœจ", "info"); updateStatus(ctx); return; } ctx.ui.notify(`Unknown command: ${command}`, "error"); }, }); // Register test flag pi.registerFlag("nanny-test-time", { description: "Test mode: simulate specific hour (0-23)", type: "string", }); // Keyboard shortcut: Quick snooze pi.registerShortcut("ctrl+shift+s", { description: "Quick snooze Pi-Nanny (15 min)", handler: async (ctx) => { if (isPastHardExit()) { ctx.ui.notify(`๐Ÿšจ Too late! Past ${config.hardExitHour}:00 AM. No snooze allowed!`, "error"); return; } if (!isBedtime()) { ctx.ui.notify("Not currently in bedtime hours", "info"); return; } const hour = getCurrentHour(); const now = new Date(); const snoozeEnd = new Date(Date.now() + 15 * 60000); // Check if snooze would go past hard exit (skip check in test mode) if (!config.testMode && snoozeEnd.getHours() >= config.hardExitHour && hour < config.hardExitHour) { const maxMinutes = (config.hardExitHour - hour) * 60 - now.getMinutes(); ctx.ui.notify( `โš ๏ธ Can't snooze past ${config.hardExitHour}:00 AM. Max: ${maxMinutes} min`, "warning", ); return; } config.snoozeUntil = Date.now() + 15 * 60000; config.warningCount = Math.max(0, config.warningCount - 2); ctx.ui.notify("๐Ÿ˜ด Quick snooze activated (15 min)", "info"); updateStatus(ctx); }, }); }