ktg-plugin-marketplace/plugins/linkedin-thought-leadership/hooks/scripts/session-start.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

404 lines
17 KiB
JavaScript

#!/usr/bin/env node
// SessionStart hook for linkedin-thought-leadership plugin
// Reads persistent state and session context, outputs JSON with additionalContext
import { readFileSync, existsSync, copyFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { calculateScore } from './personalization-score.mjs';
import { queueToday, queueOverdue, queueUpcoming } 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');
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);
}
function isoWeek() {
const d = new Date();
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNo = Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
}
function dayOfWeek() {
const d = new Date().getDay();
return d === 0 ? 7 : d; // 1=Mon, 7=Sun (ISO)
}
let context = '';
if (existsSync(STATE_FILE)) {
const stateContent = readFileSync(STATE_FILE, 'utf-8');
// Extract YAML frontmatter values
const lastPostDate = extractYaml(stateContent, 'last_post_date');
const lastPostTopic = extractYaml(stateContent, 'last_post_topic');
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 currentWeek = extractYaml(stateContent, 'current_week');
const nextPlannedTopic = extractYaml(stateContent, 'next_planned_topic');
const lastImportDate = extractYaml(stateContent, 'last_import_date');
const firstPostDate = extractYaml(stateContent, 'first_post_date');
const followerCount = parseInt(extractYaml(stateContent, 'follower_count') || '0', 10);
const followerTarget = parseInt(extractYaml(stateContent, 'follower_target') || '10000', 10);
const targetDate = extractYaml(stateContent, 'target_date');
const growthRateNeeded = parseInt(extractYaml(stateContent, 'growth_rate_needed') || '0', 10);
const projected10kDate = extractYaml(stateContent, 'projected_10k_date');
// Calculate days since last post
const daysSincePost = daysSince(lastPostDate);
const daysSinceImport = daysSince(lastImportDate);
const daysSinceFirstPost = daysSince(firstPostDate);
// New creator boost window
let boostWindowStatus = '';
let boostDaysRemaining = 0;
if (daysSinceFirstPost !== null) {
if (daysSinceFirstPost <= 90) {
boostWindowStatus = 'ACTIVE';
boostDaysRemaining = 90 - daysSinceFirstPost;
} else if (daysSinceFirstPost <= 120) {
boostWindowStatus = 'TRANSITION';
} else {
boostWindowStatus = 'ESTABLISHED';
}
}
// Milestone metrics
let milestonePhase = '';
let milestoneStatus = '';
let followersNeeded = 0;
let monthsRemaining = 0;
let ratePerMonth = 0;
let phaseTransitionAlert = '';
if (followerCount > 0) {
if (followerCount < 1000) milestonePhase = 'Foundation';
else if (followerCount < 3000) milestonePhase = 'Validation';
else if (followerCount < 6000) milestonePhase = 'Acceleration';
else if (followerCount < 10000) milestonePhase = 'Authority';
else milestonePhase = 'Scale';
// Phase transition proximity
const thresholds = [
{ limit: 1000, label: 'Validation phase (1,000)' },
{ limit: 3000, label: 'Acceleration phase (3,000)' },
{ limit: 6000, label: 'Authority phase (6,000)' },
{ limit: 10000, label: 'Scale phase (10,000)' }
];
for (const { limit, label } of thresholds) {
if (followerCount < limit && followerCount >= limit * 0.9) {
phaseTransitionAlert = `${limit - followerCount} followers to ${label}`;
break;
}
}
followersNeeded = Math.max(0, followerTarget - followerCount);
// Calculate months remaining to target_date
if (targetDate && targetDate !== 'null' && targetDate !== '""') {
const [tYear, tMonth] = targetDate.split('-').map(Number);
const now = new Date();
monthsRemaining = (tYear - now.getFullYear()) * 12 + (tMonth - (now.getMonth() + 1));
if (monthsRemaining < 1) monthsRemaining = 1;
ratePerMonth = Math.floor(followersNeeded / monthsRemaining);
}
// Schedule status
if (followerCount >= followerTarget) {
milestoneStatus = 'ACHIEVED';
} else if (growthRateNeeded > 0 && monthsRemaining > 0) {
if (ratePerMonth > growthRateNeeded * 2) milestoneStatus = 'SIGNIFICANTLY BEHIND';
else if (ratePerMonth > growthRateNeeded * 1.2) milestoneStatus = 'BEHIND';
else if (ratePerMonth < growthRateNeeded * 0.8) milestoneStatus = 'AHEAD';
else milestoneStatus = 'ON TRACK';
} else if (followerCount >= followerTarget) {
milestoneStatus = 'ACHIEVED';
} else {
milestoneStatus = 'TRACKING';
}
}
// Week rollover check
const actualWeek = isoWeek();
let weekResetNote = '';
if (currentWeek && currentWeek !== actualWeek) {
weekResetNote = `Note: Week has changed from ${currentWeek} to ${actualWeek}. posts_this_week should be reset to 0.`;
}
// Build status line
let statusLine = `LinkedIn: ${postsThisWeek}/${weeklyGoal} posts this week | Streak: ${currentStreak} days`;
if (lastPostDate && lastPostDate !== 'null') {
statusLine += ` | Last: ${lastPostDate}`;
if (daysSincePost !== null) statusLine += ` (${daysSincePost} days ago)`;
}
if (lastImportDate && lastImportDate !== 'null' && daysSinceImport !== null) {
statusLine += ` | Import: ${daysSinceImport}d ago`;
} else {
statusLine += ' | Import: never';
}
if (milestonePhase && followerCount > 0) {
statusLine += ` | ${followerCount}/${followerTarget} followers (${milestonePhase})`;
}
// Personalization score
let pScore = null;
try {
const { score } = calculateScore(PLUGIN_ROOT);
pScore = score;
statusLine += ` | Personalization: ${score}%`;
} catch { /* ignore */ }
// New creator window
if (boostWindowStatus === 'ACTIVE') {
statusLine += ` | NEW CREATOR: ${boostDaysRemaining}d left`;
}
// Load queue data
let queueTodayEntries = [];
let queueOverdueEntries = [];
let queueUpcomingCount = 0;
try {
queueTodayEntries = queueToday();
queueOverdueEntries = queueOverdue();
queueUpcomingCount = queueUpcoming(7).length;
} catch { /* ignore */ }
const queueTodayCount = queueTodayEntries.length;
const queueOverdueCount = queueOverdueEntries.length;
let queueTodayText = '';
if (queueTodayCount > 0) {
queueTodayText = queueTodayEntries.map(e => {
const t = e.scheduled_time || '?';
const hook = (e.hook_preview || '').slice(0, 50);
const pillar = e.pillar || '?';
const fmt = e.format || '?';
return ` ${t}: "${hook}..." — ${pillar} (${fmt})`;
}).join('\n');
}
let queueOverdueText = '';
if (queueOverdueCount > 0) {
queueOverdueText = queueOverdueEntries.map(e => {
const d = e.scheduled_date || '?';
const hook = (e.hook_preview || '').slice(0, 50);
const pillar = e.pillar || '?';
return ` ${d}: "${hook}..." — ${pillar}`;
}).join('\n');
}
// Build context output
context = 'LinkedIn Thought Leadership session context loaded.\\n\\n';
context += `## Status\\n\`\`\`\\n${statusLine}\\n\`\`\`\\n\\n`;
if (weekResetNote) context += `**${weekResetNote}**\\n\\n`;
if (nextPlannedTopic) context += `**Planned next topic:** ${nextPlannedTopic}\\n\\n`;
if (lastPostTopic) context += `**Last post topic:** ${lastPostTopic}\\n\\n`;
// Recent posts section
const recentMatch = stateContent.match(/^## Recent Posts\n([\s\S]*?)(?=\n## [^R]|\n## $|$)/m);
if (recentMatch) {
const recentPosts = recentMatch[1].split('\n').slice(0, 10).join('\n');
if (recentPosts.trim()) context += `## Recent Posts\\n${recentPosts.replace(/\n/g, '\\n')}\\n\\n`;
}
// Today's scheduled posts
if (queueTodayText) {
context += `## Today's Scheduled Posts\\n${queueTodayText.replace(/\n/g, '\\n')}\\nRun /linkedin:publish after posting to update tracking.\\n\\n`;
}
// Overdue posts
if (queueOverdueText) {
context += `## OVERDUE Posts\\n${queueOverdueText.replace(/\n/g, '\\n')}\\nRun /linkedin:publish to mark as posted, or /linkedin:calendar to reschedule.\\n\\n`;
}
// Posting reminders
let reminders = '';
if (daysSincePost !== null) {
if (daysSincePost >= 3) {
reminders += `- No LinkedIn post in ${daysSincePost} days. Posting gaps >5 days reduce reach by 15-25%. Consider /linkedin:quick or /linkedin:pipeline.\\n`;
}
if (daysSincePost >= 2 && currentStreak > 3) {
reminders += `- Your ${currentStreak}-day posting streak is at risk! Post today to keep momentum.\\n`;
}
}
// First-post nudge
if ((!firstPostDate || firstPostDate === 'null') && postsThisWeek === 0) {
reminders += '- First post not yet created! Run /linkedin:first-post to publish your first LinkedIn post in under 10 minutes.\\n';
}
// Weekly goal check
const weekRemaining = weeklyGoal - postsThisWeek;
const dow = dayOfWeek();
if (weekRemaining > 0 && dow >= 4) {
reminders += `- ${weekRemaining} posts remaining to hit weekly goal of ${weeklyGoal}. It's late in the week.\\n`;
}
// Personalization score check
if (pScore !== null && pScore < 50) {
reminders += `- Personalization score is ${pScore}%. Run /linkedin:setup to improve content quality with your real voice, case studies, and audience data.\\n`;
}
// Import staleness
if (daysSinceImport !== null) {
if (daysSinceImport >= 14) {
reminders += `- Analytics data is ${daysSinceImport} days stale. Strategy recommendations may be inaccurate. Run /linkedin:import.\\n`;
} else if (daysSinceImport >= 7) {
reminders += `- Last analytics import was ${daysSinceImport} days ago. Consider /linkedin:import for fresh data.\\n`;
}
} else if (!lastImportDate || lastImportDate === 'null') {
reminders += '- No analytics data imported yet. Run /linkedin:import to start tracking performance.\\n';
}
// Milestone reminders
if (milestonePhase && followerCount > 0) {
if (milestoneStatus === 'SIGNIFICANTLY BEHIND') {
reminders += `- 10K milestone: SIGNIFICANTLY BEHIND schedule. Need ~${ratePerMonth} followers/month (2x+ original rate). Run /linkedin:strategy for corrective adjustments — current approach needs a fundamental shift.\\n`;
} else if (milestoneStatus === 'BEHIND') {
reminders += `- 10K milestone: BEHIND schedule. Need ~${ratePerMonth} followers/month. Consider /linkedin:strategy for trajectory-based adjustments.\\n`;
} else if (milestoneStatus === 'AHEAD') {
reminders += '- 10K milestone: AHEAD of schedule. Consider raising target or shifting focus to monetization (/linkedin:monetize).\\n';
}
} else if (!followerCount || followerCount === 0) {
reminders += '- No follower count tracked yet. Update follower_count in state file to enable 10K milestone tracking.\\n';
}
// Phase transition proximity
if (phaseTransitionAlert) {
reminders += `- PHASE TRANSITION: ${phaseTransitionAlert}. Run /linkedin:strategy to prepare.\\n`;
}
// New creator advantage window
if (boostWindowStatus === 'ACTIVE') {
if (boostDaysRemaining < 14) {
reminders += `- NEW CREATOR WINDOW CLOSING: Only ${boostDaysRemaining} days left! Maximize posting frequency (4-5x/week) and engagement (15-20 comments/day) now.\\n`;
} else if (boostDaysRemaining < 30) {
reminders += `- New creator window: ${boostDaysRemaining} days remaining. Maintain high frequency (4-5x/week) to lock in algorithmic momentum.\\n`;
} else {
reminders += `- New creator advantage active (${boostDaysRemaining}d left). Higher posting frequency pays outsized returns during this window.\\n`;
}
} else if (boostWindowStatus === 'TRANSITION') {
reminders += `- New creator window ended ${daysSinceFirstPost} days ago. Transition to sustainable posting rhythm (3-4x/week) and optimize based on analytics.\\n`;
}
// Queue-related reminders
if (queueTodayCount > 0) {
reminders += `- You have ${queueTodayCount} post(s) scheduled for today. Run /linkedin:publish after posting.\\n`;
}
if (queueOverdueCount > 0) {
reminders += `- ${queueOverdueCount} overdue post(s) in queue. Run /linkedin:publish or /linkedin:calendar to manage.\\n`;
}
if (reminders) context += `## Posting Reminders\\n${reminders}\\n`;
// 10K Milestone Tracker section
if (milestonePhase && followerCount > 0) {
context += '## 10K Milestone Tracker\\n';
context += `- Current: ${followerCount} followers (Phase: ${milestonePhase})\\n`;
if (monthsRemaining > 0 && followersNeeded > 0) {
context += `- Required rate: ~${ratePerMonth} followers/month to hit ${followerTarget} by ${targetDate}\\n`;
}
if (milestoneStatus) context += `- Status: ${milestoneStatus}\\n`;
if (projected10kDate && projected10kDate !== 'null' && projected10kDate !== '""') {
context += `- Projected: ${projected10kDate} (at current rate)\\n`;
}
if (phaseTransitionAlert) context += `- PHASE TRANSITION: ${phaseTransitionAlert}\\n`;
if (milestoneStatus === 'SIGNIFICANTLY BEHIND') {
context += '- Trajectory hint: Current approach needs fundamental adjustment. Run /linkedin:strategy for corrective plan.\\n';
} else if (milestoneStatus === 'BEHIND') {
context += '- Trajectory hint: Consider /linkedin:strategy for trajectory-based adjustments to close the gap.\\n';
} else if (milestoneStatus === 'AHEAD') {
context += '- Trajectory hint: Strong momentum. Consider raising target or shifting to monetization (/linkedin:monetize).\\n';
}
context += '\\n';
}
// New creator advantage window context
if (boostWindowStatus === 'ACTIVE') {
context += '## New Creator Advantage Window\\n';
context += `- Status: ACTIVE (day ${daysSinceFirstPost} of 90, ${boostDaysRemaining} days remaining)\\n`;
context += `- First post: ${firstPostDate}\\n`;
context += '- Recommended frequency: 4-5x/week (vs standard 3x)\\n';
context += '- Recommended engagement: 15-20 strategic comments/day\\n';
context += '- Priority: Save-worthy content (frameworks, checklists, templates)\\n\\n';
} else if (boostWindowStatus === 'TRANSITION') {
context += '## New Creator Advantage Window\\n';
context += `- Status: TRANSITION (day ${daysSinceFirstPost}, window closed at day 90)\\n`;
context += '- Shift to sustainable rhythm: 3-4x/week, optimize based on analytics data\\n\\n';
}
// Queue summary
if (queueUpcomingCount > 0) {
context += '## Queue Summary\\n';
context += `- Queued posts (next 7 days): ${queueUpcomingCount}\\n`;
if (queueTodayCount > 0) context += `- Today: ${queueTodayCount} post(s)\\n`;
if (queueOverdueCount > 0) context += `- Overdue: ${queueOverdueCount} post(s)\\n`;
context += '- Manage: /linkedin:calendar | Publish: /linkedin:publish\\n\\n';
}
context += `State file: ${STATE_FILE}\\n`;
} else {
// Auto-initialize state file from template
const templateFile = join(PLUGIN_ROOT, 'config', 'state-file.template.md');
if (existsSync(templateFile)) {
mkdirSync(dirname(STATE_FILE), { recursive: true });
copyFileSync(templateFile, STATE_FILE);
const actualWeek = isoWeek();
let content = readFileSync(STATE_FILE, 'utf-8');
content = content.replace(/^current_week: .*/m, `current_week: "${actualWeek}"`);
writeFileSync(STATE_FILE, content);
context = `LinkedIn state file auto-initialized from template at ${STATE_FILE}.\\n`;
context += `Current ISO week set to ${actualWeek}.\\n`;
context += 'Edit the file to set your expertise_areas and weekly_goal.\\n';
} else {
context = `No LinkedIn state file found at ${STATE_FILE} and template missing.\\n`;
context += `Expected template at: ${templateFile}\\n`;
}
}
// Read REMEMBER.md for user session context
const rememberFile = join(PLUGIN_ROOT, 'REMEMBER.md');
const rememberTemplate = join(PLUGIN_ROOT, 'config', 'REMEMBER.template.md');
if (!existsSync(rememberFile) && existsSync(rememberTemplate)) {
copyFileSync(rememberTemplate, rememberFile);
let rememberContent = readFileSync(rememberFile, 'utf-8');
const today = new Date().toISOString().slice(0, 10);
rememberContent = rememberContent.replace('[Auto-filled by session-start.sh]', today);
writeFileSync(rememberFile, rememberContent);
context += '\\n## Session State\\nREMEMBER.md auto-initialized from template. Update after your first session.\\n';
} else if (existsSync(rememberFile)) {
const rememberContent = readFileSync(rememberFile, 'utf-8');
const rememberSummary = rememberContent.split('\n').slice(0, 50).join('\n');
context += `\\n## Session Context (from REMEMBER.md)\\n${rememberSummary.replace(/\n/g, '\\n')}\\n`;
}
// Output JSON for Claude Code
const output = {
continue: true,
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: context.replace(/\\n/g, '\n')
}
};
process.stdout.write(JSON.stringify(output));