feat(linkedin-thought-leadership): v1.1.0 — Q2 2026 feature release

9 improvements across 3 tracks:

Onboarding: /linkedin:onboarding wizard, README Quick Start rewrite
Content Quality: voice drift scoring, industry angle variants,
  /linkedin:carousel, /linkedin:react multi-URL comparison
Analytics: automated week-rollover, day-of-week heatmap,
  month-over-month reports

25→27 commands. All Q2 ROADMAP items completed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-04-08 06:16:35 +02:00
commit 1a8cc1942c
33 changed files with 1726 additions and 236 deletions

View file

@ -8,6 +8,8 @@ import {
import { detectAlerts } from "./utils/alerts.js";
import { mean, standardDeviation } from "./utils/stats.js";
import { generateWeeklyReport, getCurrentISOWeek } from "./reports/weekly.js";
import { generateHeatmap } from "./reports/heatmap.js";
import { generateMonthlyReport } from "./reports/monthly.js";
import { join } from "node:path";
import { existsSync } from "node:fs";
import type { PostMetrics } from "./models/types.js";
@ -27,7 +29,9 @@ LinkedIn Analytics CLI
Usage:
node build/cli.js import <filename> Import a CSV export
node build/cli.js report [--week W] Generate weekly report
node build/cli.js report --month YYYY-MM Generate monthly report with MoM comparison
node build/cli.js trends [--period P] [--metric M] Show trends and alerts
node build/cli.js heatmap Day-of-week performance matrix
Options:
--week W ISO week (e.g., 2026-W05), defaults to current week
@ -95,6 +99,11 @@ async function handleImport(root: string, args: string[]) {
}
async function handleReport(root: string, args: string[]) {
const monthOption = parseOption(args, "--month");
if (monthOption) {
return handleMonthlyReport(root, monthOption);
}
const weekOption = parseOption(args, "--week");
const week = weekOption || getCurrentISOWeek();
@ -285,6 +294,130 @@ async function handleTrends(root: string, args: string[]) {
}
}
async function handleMonthlyReport(root: string, month: string) {
console.log(`Generating monthly report for ${month}...`);
try {
const report = generateMonthlyReport(root, month);
console.log("\nMonthly Report");
console.log("═════════════════════════════════════");
console.log(`Month: ${report.month}`);
console.log(`Generated at: ${new Date(report.generatedAt).toLocaleString()}`);
console.log();
console.log("Summary");
console.log("─────────────────────────────────────");
const s = report.summary;
const fmtDelta = (val: number | null, suffix = "%") =>
val !== null ? ` (${val > 0 ? "+" : ""}${val}${suffix})` : "";
console.log(`Posts: ${s.totalPosts}${fmtDelta(report.trends.percentChange.postCount)}`);
console.log(`Impressions: ${s.totalImpressions.toLocaleString()}${fmtDelta(report.trends.percentChange.impressions)}`);
console.log(`Avg per post: ${s.avgImpressionsPerPost.toLocaleString()}`);
console.log(`Avg engagement: ${s.avgEngagementRate.toFixed(2)}%${fmtDelta(report.trends.percentChange.engagement)}`);
console.log(`Reactions: ${s.totalReactions.toLocaleString()}`);
console.log(`Comments: ${s.totalComments.toLocaleString()}`);
console.log(`Shares: ${s.totalShares.toLocaleString()}`);
console.log(`Clicks: ${s.totalClicks.toLocaleString()}`);
console.log();
if (report.byWeek.length > 0) {
console.log("Week Breakdown");
console.log("─────────────────────────────────────");
for (const w of report.byWeek) {
console.log(`${w.week}: ${w.postCount} posts | ${w.avgImpressions.toLocaleString()} avg impr | ${w.avgEngagementRate.toFixed(1)}% eng`);
}
console.log();
}
if (report.topPerformers.length > 0) {
console.log("Top Performers");
console.log("─────────────────────────────────────");
for (const post of report.topPerformers.slice(0, 5)) {
const title = post.title.length > 50 ? post.title.substring(0, 47) + "..." : post.title;
console.log(`${title}`);
console.log(` ${post.metrics.impressions.toLocaleString()} impressions | ${post.metrics.engagementRate.toFixed(2)}% eng | ${post.publishedDate}`);
}
console.log();
}
if (report.trends.comparedTo) {
console.log(`Compared to: ${report.trends.comparedTo}`);
} else {
console.log("No previous month data for comparison.");
}
console.log();
if (report.alerts.length > 0) {
console.log("Alerts");
console.log("─────────────────────────────────────");
for (const alert of report.alerts) {
const icon = alert.severity === "critical" ? "🔴" : alert.severity === "warning" ? "⚠️" : "";
console.log(`${icon} [${alert.severity.toUpperCase()}] ${alert.message}`);
}
console.log();
}
console.log(`Report saved to: monthly-reports/${month}.json`);
} catch (err) {
console.error(`Error generating monthly report: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
}
async function handleHeatmap(root: string) {
console.log("Generating day-of-week heatmap...");
try {
const allPosts = loadAllPosts(root);
if (allPosts.length === 0) {
console.error("Error: No posts found. Import some data first.");
process.exit(1);
}
const report = generateHeatmap(allPosts);
console.log("\nDay-of-Week Performance Heatmap");
console.log("═════════════════════════════════════");
console.log(`Posts analyzed: ${report.postsAnalyzed}`);
console.log(`Date range: ${report.dateRange.from} to ${report.dateRange.to}`);
console.log();
// Print table header
const days = report.byDayOfWeek.map(d => d.dayName.slice(0, 3).padStart(7));
console.log(` ${days.join("")}`);
console.log(` ${"───────".repeat(7)}`);
// Posts row
const postCounts = report.byDayOfWeek.map(d => String(d.postCount).padStart(7));
console.log(`Posts: ${postCounts.join("")}`);
// Impressions row
const impressions = report.byDayOfWeek.map(d =>
d.postCount > 0 ? d.avgImpressions.toLocaleString().padStart(7) : " -"
);
console.log(`Impr: ${impressions.join("")}`);
// Engagement rate row
const engRates = report.byDayOfWeek.map(d =>
d.postCount > 0 ? `${d.avgEngagementRate.toFixed(1)}%`.padStart(7) : " -"
);
console.log(`Eng: ${engRates.join("")}`);
console.log();
console.log(`Best day for impressions: ${report.bestDayImpressions}`);
console.log(`Best day for engagement: ${report.bestDayEngagement}`);
console.log("\nNote: LinkedIn CSV exports do not include publish time.");
console.log("This heatmap shows day-of-week only.");
} catch (err) {
console.error(`Error generating heatmap: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
}
async function main() {
const root = getAnalyticsRoot();
ensureDirectories(root);
@ -299,6 +432,9 @@ async function main() {
case "trends":
await handleTrends(root, args);
break;
case "heatmap":
await handleHeatmap(root);
break;
default:
printUsage();
process.exit(command ? 1 : 0);