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>
This commit is contained in:
parent
7194a37129
commit
39f8b275a6
143 changed files with 32662 additions and 0 deletions
|
|
@ -0,0 +1,404 @@
|
|||
#!/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));
|
||||
Loading…
Add table
Add a link
Reference in a new issue