ktg-plugin-marketplace/plugins/linkedin-thought-leadership/hooks/scripts/posting-reminder.mjs
Kjell Tore Guttormsen 39f8b275a6 feat(linkedin-thought-leadership): v1.0.0 — initial open-source import
Build LinkedIn thought leadership with algorithmic understanding,
strategic consistency, and AI-assisted content creation. Updated for
the January 2026 360Brew algorithm change.

16 agents, 25 commands, 6 skills, 9 hooks, 24 reference docs.

Personal data sanitized: voice samples generalized to template,
high-engagement posts cleared, region-specific references replaced
with placeholders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 22:09:03 +02:00

112 lines
4.9 KiB
JavaScript

#!/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('{}');
}