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:
Kjell Tore Guttormsen 2026-04-07 22:09:03 +02:00
commit 39f8b275a6
143 changed files with 32662 additions and 0 deletions

View file

@ -0,0 +1,233 @@
import type { PostAnalytics, WeeklyReport } from "../models/types.js";
import { mean, trendDirection, percentChange } from "../utils/stats.js";
import { detectAlerts, detectWeeklyAlerts } from "../utils/alerts.js";
import { loadAllPosts, loadWeeklyReport, saveWeeklyReport } from "../utils/storage.js";
/**
* Get current ISO week string (e.g., "2026-W05").
* Uses ISO 8601 week date system where Monday is first day of week.
*/
export function getCurrentISOWeek(): string {
return getISOWeek(new Date());
}
/**
* Get ISO week string for a specific date.
* Format: "YYYY-WXX" where XX is zero-padded week number.
*
* ISO 8601 week date rules:
* - Week starts on Monday
* - Week 1 is the week with the first Thursday of the year
* - Last week of year might extend into next year
*/
export function getISOWeek(date: Date): string {
// Copy date to avoid mutating original
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
// Set to nearest Thursday: current date + 4 - current day number
// Make Sunday's day number 7
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
// Get first day of year
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
// Calculate full weeks to nearest Thursday
const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
// Return ISO week format
const year = d.getUTCFullYear();
const weekStr = weekNo.toString().padStart(2, '0');
return `${year}-W${weekStr}`;
}
/**
* Filter posts to a specific ISO week.
* Posts are matched by converting their publishedDate to ISO week format.
*/
export function getPostsForWeek(posts: PostAnalytics[], week: string): PostAnalytics[] {
return posts.filter(post => {
const postDate = new Date(post.publishedDate);
const postWeek = getISOWeek(postDate);
return postWeek === week;
});
}
/**
* Get the ISO week string for the previous week.
* Uses proper ISO week calculation to handle year boundaries correctly.
*/
function getPreviousWeek(week: string): string {
// Parse week string (e.g., "2026-W05")
const match = week.match(/^(\d{4})-W(\d{2})$/);
if (!match) {
throw new Error(`Invalid week format: ${week}`);
}
const year = parseInt(match[1]);
const weekNum = parseInt(match[2]);
// ISO week 1 is the week containing January 4th
// Find Thursday of the target ISO week
const jan4 = new Date(Date.UTC(year, 0, 4));
// Find Monday of week 1 by going back from Jan 4 to Monday
const jan4Day = jan4.getUTCDay() || 7; // Sunday = 7 in ISO
const week1Monday = new Date(jan4.getTime() - (jan4Day - 1) * 24 * 60 * 60 * 1000);
// Add (weekNum - 1) * 7 days to get Monday of target week
const targetMonday = new Date(week1Monday.getTime() + (weekNum - 1) * 7 * 24 * 60 * 60 * 1000);
// Add 3 days to get Thursday of target week
const targetThursday = new Date(targetMonday.getTime() + 3 * 24 * 60 * 60 * 1000);
// Subtract 7 days to get previous week's Thursday
const previousThursday = new Date(targetThursday.getTime() - 7 * 24 * 60 * 60 * 1000);
// Use getISOWeek to get the correct ISO week string
return getISOWeek(previousThursday);
}
/**
* Generate a weekly report from imported analytics data.
*
* @param analyticsRoot - Root directory containing analytics data
* @param week - ISO week string (e.g., "2026-W05"). If not provided, uses current week.
* @returns WeeklyReport object
*
* Process:
* 1. Load all posts from storage
* 2. Filter posts for target week
* 3. Calculate summary metrics
* 4. Find top 3 performers and bottom 3 underperformers
* 5. Calculate trends vs previous week
* 6. Generate alerts
* 7. Save and return report
*
* Edge cases:
* - No posts for week zeroed summary
* - No previous week data stable trends with 0% change
* - Fewer than 3 posts shorter top/bottom lists
*/
export function generateWeeklyReport(analyticsRoot: string, week?: string): WeeklyReport {
// Determine target week
const targetWeek = week || getCurrentISOWeek();
// Load all posts
const allPosts = loadAllPosts(analyticsRoot);
// Filter posts for target week
const weekPosts = getPostsForWeek(allPosts, targetWeek);
// Initialize report structure
const report: WeeklyReport = {
week: targetWeek,
generatedAt: new Date().toISOString(),
summary: {
totalPosts: weekPosts.length,
totalImpressions: 0,
totalReactions: 0,
totalComments: 0,
totalShares: 0,
totalClicks: 0,
avgEngagementRate: 0,
avgImpressionsPerPost: 0,
},
topPerformers: [],
underperformers: [],
trends: {
impressionsTrend: "stable",
engagementTrend: "stable",
comparedTo: getPreviousWeek(targetWeek),
percentChange: {
impressions: 0,
engagement: 0,
},
},
alerts: [],
};
// If no posts, return early with zeroed report
if (weekPosts.length === 0) {
return report;
}
// Calculate summary metrics
for (const post of weekPosts) {
report.summary.totalImpressions += post.metrics.impressions;
report.summary.totalReactions += post.metrics.reactions;
report.summary.totalComments += post.metrics.comments;
report.summary.totalShares += post.metrics.shares;
report.summary.totalClicks += post.metrics.clicks;
}
// Calculate averages
const engagementRates = weekPosts.map(post => post.metrics.engagementRate);
report.summary.avgEngagementRate = mean(engagementRates);
report.summary.avgImpressionsPerPost = report.summary.totalImpressions / weekPosts.length;
// Find top 3 performers (highest engagement rate)
const sortedByEngagement = [...weekPosts].sort(
(a, b) => b.metrics.engagementRate - a.metrics.engagementRate
);
report.topPerformers = sortedByEngagement.slice(0, 3);
// Find bottom 3 underperformers (lowest engagement rate)
report.underperformers = sortedByEngagement
.slice()
.reverse()
.slice(0, 3);
// Calculate trends vs previous week
const previousWeek = getPreviousWeek(targetWeek);
const previousReport = loadWeeklyReport(analyticsRoot, previousWeek);
if (previousReport && previousReport.summary.totalPosts > 0) {
// Calculate percent changes
report.trends.percentChange.impressions = percentChange(
report.summary.totalImpressions,
previousReport.summary.totalImpressions
);
report.trends.percentChange.engagement = percentChange(
report.summary.avgEngagementRate,
previousReport.summary.avgEngagementRate
);
// Determine trend directions
report.trends.impressionsTrend = trendDirection(
report.summary.totalImpressions,
previousReport.summary.totalImpressions
);
report.trends.engagementTrend = trendDirection(
report.summary.avgEngagementRate,
previousReport.summary.avgEngagementRate
);
}
// Generate alerts
const postAlerts = detectAlerts(weekPosts, "impressions");
let weeklyAlerts: typeof report.alerts = [];
if (previousReport && previousReport.summary.totalPosts > 0) {
weeklyAlerts = detectWeeklyAlerts(
{
impressions: report.summary.totalImpressions,
engagementRate: report.summary.avgEngagementRate,
},
{
impressions: previousReport.summary.totalImpressions,
engagementRate: previousReport.summary.avgEngagementRate,
}
);
}
report.alerts = [...weeklyAlerts, ...postAlerts];
// Save report
saveWeeklyReport(analyticsRoot, report);
return report;
}