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>
112 lines
4.9 KiB
JavaScript
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('{}');
|
|
}
|