#!/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'; import { applyWeekRollover } from './week-rollover.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 — auto-reset posts_this_week on week change const actualWeek = isoWeek(); let weekResetNote = ''; try { const rollover = applyWeekRollover(stateContent, currentWeek, actualWeek); if (rollover) { writeFileSync(STATE_FILE, rollover.content, 'utf-8'); weekResetNote = rollover.message; } } catch (err) { weekResetNote = `Warning: Week rollover failed (${err.message}). Manual reset may be needed.`; } // Auto-prune Recent Posts entries older than 90 days try { const currentState = readFileSync(STATE_FILE, 'utf-8'); const { pruneContentHistory } = await import('./state-updater.mjs'); const pruneResult = pruneContentHistory(currentState, 90); if (pruneResult && pruneResult.pruned > 0) { writeFileSync(STATE_FILE, pruneResult.content, 'utf-8'); weekResetNote += (weekResetNote ? ' ' : '') + `Auto-pruned ${pruneResult.pruned} posts older than 90 days from Recent Posts.`; } } catch { // Non-critical: don't block session start on pruning failure } // Count published posts for progressive onboarding const recentPostsSection = stateContent.match(/^## Recent Posts\n([\s\S]*?)(?=\n## [^R]|\n## $|$)/m); let publishedPostCount = 0; if (recentPostsSection) { publishedPostCount = (recentPostsSection[1].match(/^\s*[-\[]/gm) || []).length; } // 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 (only show after 3+ published posts — progressive onboarding) let pScore = null; try { const { score } = calculateScore(PLUGIN_ROOT); pScore = score; if (publishedPostCount >= 3) { 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 (only after 3+ posts — progressive onboarding) if (pScore !== null && pScore < 50 && publishedPostCount >= 3) { 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));