#!/usr/bin/env node // Notification hook for linkedin-thought-leadership plugin // Fires on idle_prompt to show posting reminders. Rate-limited: max once per 30 min. import { readFileSync, existsSync, statSync, writeFileSync, mkdirSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { queueToday, queueOverdue } from './queue-manager.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PLUGIN_ROOT = join(__dirname, '..', '..'); const HOME = process.env.HOME || process.env.USERPROFILE || ''; const STATE_FILE = join(HOME, '.claude', 'linkedin-thought-leadership.local.md'); const SESSION_DIR = '/tmp/linkedin-hooks'; const COOLDOWN_FILE = join(SESSION_DIR, 'last-notification'); const COOLDOWN_SECONDS = 1800; function extractYaml(content, key) { const re = new RegExp(`^${key}: *"?([^"\\n]*)"?`, 'm'); const m = content.match(re); return m ? m[1].trim() : ''; } function daysSince(dateStr) { if (!dateStr || dateStr === 'null') return null; const epoch = new Date(dateStr).getTime(); if (isNaN(epoch)) return null; return Math.floor((Date.now() - epoch) / 86400000); } // Read stdin let input; try { input = JSON.parse(readFileSync(0, 'utf-8')); } catch { process.exit(0); } if ((input.notification_type || '') !== 'idle_prompt') process.exit(0); // Rate limiting if (existsSync(COOLDOWN_FILE)) { const age = (Date.now() - statSync(COOLDOWN_FILE).mtime.getTime()) / 1000; if (age < COOLDOWN_SECONDS) process.exit(0); } if (!existsSync(STATE_FILE)) process.exit(0); const stateContent = readFileSync(STATE_FILE, 'utf-8'); const lastPostDate = extractYaml(stateContent, 'last_post_date'); const postsThisWeek = parseInt(extractYaml(stateContent, 'posts_this_week') || '0', 10); const weeklyGoal = parseInt(extractYaml(stateContent, 'weekly_goal') || '3', 10); const currentStreak = parseInt(extractYaml(stateContent, 'current_streak') || '0', 10); const lastImportDate = extractYaml(stateContent, 'last_import_date'); const followerCount = parseInt(extractYaml(stateContent, 'follower_count') || '0', 10); const followerTarget = parseInt(extractYaml(stateContent, 'follower_target') || '10000', 10); const reminders = []; // Days since last post const dsp = daysSince(lastPostDate); if (dsp !== null) { if (dsp >= 3) reminders.push(`No LinkedIn post in ${dsp} days. Posting gaps >5 days reduce reach by 15-25%. Consider running /linkedin:quick or /linkedin:pipeline.`); if (dsp >= 2 && currentStreak > 3) reminders.push(`Your ${currentStreak}-day posting streak is at risk! Last post was ${dsp} days ago. Post today to keep momentum.`); } // Weekly goal const remaining = weeklyGoal - postsThisWeek; const dow = new Date().getDay() || 7; // 1=Mon, 7=Sun if (remaining > 0) { if (dow >= 4 && remaining >= 2) reminders.push(`${remaining} posts remaining to hit your weekly goal of ${weeklyGoal}. It's already late in the week — consider /linkedin:batch to catch up.`); if (dow >= 5 && remaining >= 1) reminders.push(`Weekly goal: ${postsThisWeek}/${weeklyGoal} posts. ${remaining} to go before the week ends.`); } // Import staleness const dsi = daysSince(lastImportDate); if (dsi !== null) { if (dsi >= 14) reminders.push(`Analytics data is ${dsi} days stale. Run /linkedin:import to update your performance data.`); else if (dsi >= 7) reminders.push(`Have you imported this week's LinkedIn data? Last import was ${dsi} days ago. Run /linkedin:import.`); } else { reminders.push('No LinkedIn analytics imported yet. Run /linkedin:import to start tracking performance.'); } // Milestone if (followerCount > 0 && followerTarget > 0) { const pct = Math.floor(followerCount * 100 / followerTarget); reminders.push(`10K milestone: ${followerCount}/${followerTarget} followers (${pct}% complete).`); } // Queue reminders try { const todayEntries = queueToday(); const overdueEntries = queueOverdue(); if (todayEntries.length > 0) reminders.push(`You have ${todayEntries.length} post(s) scheduled for today. Run /linkedin:publish after posting to update your tracking.`); if (overdueEntries.length > 0) reminders.push(`${overdueEntries.length} overdue post(s) in your queue. Run /linkedin:publish to mark as posted, or /linkedin:calendar to reschedule.`); } catch { /* ignore */ } // Peak posting time const hour = new Date().getHours(); if (dow >= 2 && dow <= 4) { if (hour >= 7 && hour <= 8) reminders.push('Peak posting window approaching: 8-9 AM CET on Tue-Thu is optimal for LinkedIn engagement.'); if (hour >= 11 && hour <= 12) reminders.push('Secondary peak posting window: 12-1 PM CET on Tue-Thu is good for LinkedIn engagement.'); } if (reminders.length > 0) { mkdirSync(SESSION_DIR, { recursive: true }); writeFileSync(COOLDOWN_FILE, ''); const output = 'LinkedIn Posting Reminders:\n' + reminders.map(r => `- ${r}`).join('\n'); process.stdout.write(JSON.stringify({ systemMessage: output })); } else { process.stdout.write('{}'); }