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
311
plugins/linkedin-thought-leadership/scripts/analytics/src/cli.ts
Normal file
311
plugins/linkedin-thought-leadership/scripts/analytics/src/cli.ts
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
import { parseLinkedInCSV } from "./parsers/csv-parser.js";
|
||||
import {
|
||||
getAnalyticsRoot,
|
||||
ensureDirectories,
|
||||
saveBatch,
|
||||
loadAllPosts,
|
||||
} from "./utils/storage.js";
|
||||
import { detectAlerts } from "./utils/alerts.js";
|
||||
import { mean, standardDeviation } from "./utils/stats.js";
|
||||
import { generateWeeklyReport, getCurrentISOWeek } from "./reports/weekly.js";
|
||||
import { join } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import type { PostMetrics } from "./models/types.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
function parseOption(args: string[], flag: string): string | undefined {
|
||||
const idx = args.indexOf(flag);
|
||||
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`
|
||||
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 trends [--period P] [--metric M] Show trends and alerts
|
||||
|
||||
Options:
|
||||
--week W ISO week (e.g., 2026-W05), defaults to current week
|
||||
--period P Time period: "week" | "month" | "quarter" | "all" (default: "month")
|
||||
--metric M Metric to analyze: "impressions" | "reactions" | "comments" | "shares" | "clicks" | "engagementRate" (default: "impressions")
|
||||
|
||||
Examples:
|
||||
node build/cli.js import linkedin-export-2026-01-20.csv
|
||||
node build/cli.js report --week 2026-W04
|
||||
node build/cli.js trends --period quarter --metric engagementRate
|
||||
`);
|
||||
}
|
||||
|
||||
async function handleImport(root: string, args: string[]) {
|
||||
const filename = args[1];
|
||||
|
||||
if (!filename) {
|
||||
console.error("Error: Missing filename argument");
|
||||
console.error("Usage: node build/cli.js import <filename>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fullPath = join(root, "exports", filename);
|
||||
|
||||
if (!existsSync(fullPath)) {
|
||||
console.error(`Error: File not found: ${fullPath}`);
|
||||
console.error(`\nMake sure the CSV file is placed in: ${join(root, "exports")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Importing ${filename}...`);
|
||||
|
||||
try {
|
||||
const batch = parseLinkedInCSV(fullPath, filename);
|
||||
const savedFilename = saveBatch(root, batch);
|
||||
|
||||
console.log("\nImport successful!");
|
||||
console.log("─────────────────────────────────────");
|
||||
console.log(`Posts imported: ${batch.postCount}`);
|
||||
console.log(`Date range: ${batch.dateRange.from} to ${batch.dateRange.to}`);
|
||||
console.log(`Batch ID: ${batch.batchId}`);
|
||||
console.log(`Saved to: posts/${savedFilename}`);
|
||||
|
||||
// Run alert detection on imported posts
|
||||
const alerts = detectAlerts(batch.posts, "impressions");
|
||||
|
||||
if (alerts.length > 0) {
|
||||
console.log("\nImmediate alerts detected:");
|
||||
console.log("─────────────────────────────────────");
|
||||
for (const alert of alerts.slice(0, 5)) {
|
||||
const icon = alert.severity === "critical" ? "🔴" : alert.severity === "warning" ? "⚠️" : "ℹ️";
|
||||
console.log(`${icon} [${alert.severity.toUpperCase()}] ${alert.message}`);
|
||||
}
|
||||
|
||||
if (alerts.length > 5) {
|
||||
console.log(`\n... and ${alerts.length - 5} more alerts`);
|
||||
}
|
||||
} else {
|
||||
console.log("\nNo anomalies detected in imported data.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error parsing CSV: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReport(root: string, args: string[]) {
|
||||
const weekOption = parseOption(args, "--week");
|
||||
const week = weekOption || getCurrentISOWeek();
|
||||
|
||||
console.log(`Generating weekly report for ${week}...`);
|
||||
|
||||
try {
|
||||
const report = generateWeeklyReport(root, week);
|
||||
|
||||
console.log("\nWeekly Report");
|
||||
console.log("═════════════════════════════════════");
|
||||
console.log(`Week: ${report.week}`);
|
||||
console.log(`Generated at: ${new Date(report.generatedAt).toLocaleString()}`);
|
||||
console.log();
|
||||
|
||||
console.log("Summary");
|
||||
console.log("─────────────────────────────────────");
|
||||
console.log(`Total posts: ${report.summary.totalPosts}`);
|
||||
console.log(`Total impressions: ${report.summary.totalImpressions.toLocaleString()}`);
|
||||
console.log(`Total reactions: ${report.summary.totalReactions.toLocaleString()}`);
|
||||
console.log(`Total comments: ${report.summary.totalComments.toLocaleString()}`);
|
||||
console.log(`Total shares: ${report.summary.totalShares.toLocaleString()}`);
|
||||
console.log(`Total clicks: ${report.summary.totalClicks.toLocaleString()}`);
|
||||
console.log(`Avg engagement: ${report.summary.avgEngagementRate.toFixed(2)}%`);
|
||||
console.log(`Avg impressions: ${Math.round(report.summary.avgImpressionsPerPost).toLocaleString()} per post`);
|
||||
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)}% engagement | ${post.publishedDate}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (report.underperformers.length > 0) {
|
||||
console.log("Underperformers");
|
||||
console.log("─────────────────────────────────────");
|
||||
for (const post of report.underperformers.slice(0, 3)) {
|
||||
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)}% engagement | ${post.publishedDate}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log("Trends");
|
||||
console.log("─────────────────────────────────────");
|
||||
console.log(`Impressions trend: ${report.trends.impressionsTrend.toUpperCase()} (${report.trends.percentChange.impressions > 0 ? "+" : ""}${report.trends.percentChange.impressions.toFixed(1)}%)`);
|
||||
console.log(`Engagement trend: ${report.trends.engagementTrend.toUpperCase()} (${report.trends.percentChange.engagement > 0 ? "+" : ""}${report.trends.percentChange.engagement.toFixed(1)}%)`);
|
||||
console.log(`Compared to: ${report.trends.comparedTo}`);
|
||||
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: weekly-reports/${week}.json`);
|
||||
} catch (err) {
|
||||
console.error(`Error generating report: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a string is a valid PostMetrics key
|
||||
*/
|
||||
function isPostMetric(value: string): value is keyof PostMetrics {
|
||||
const validMetrics: (keyof PostMetrics)[] = [
|
||||
"impressions",
|
||||
"reactions",
|
||||
"comments",
|
||||
"shares",
|
||||
"clicks",
|
||||
"engagementRate",
|
||||
];
|
||||
return validMetrics.includes(value as keyof PostMetrics);
|
||||
}
|
||||
|
||||
async function handleTrends(root: string, args: string[]) {
|
||||
const periodOption = parseOption(args, "--period") || "month";
|
||||
const metricOption = parseOption(args, "--metric") || "impressions";
|
||||
|
||||
const validPeriods = ["week", "month", "quarter", "all"];
|
||||
|
||||
if (!validPeriods.includes(periodOption)) {
|
||||
console.error(`Error: Invalid period "${periodOption}". Must be one of: ${validPeriods.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isPostMetric(metricOption)) {
|
||||
const validMetrics: (keyof PostMetrics)[] = [
|
||||
"impressions",
|
||||
"reactions",
|
||||
"comments",
|
||||
"shares",
|
||||
"clicks",
|
||||
"engagementRate",
|
||||
];
|
||||
console.error(`Error: Invalid metric "${metricOption}". Must be one of: ${validMetrics.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const period = periodOption as "week" | "month" | "quarter" | "all";
|
||||
const metric = metricOption;
|
||||
|
||||
console.log(`Analyzing trends for ${metric} over ${period}...`);
|
||||
|
||||
try {
|
||||
const allPosts = loadAllPosts(root);
|
||||
|
||||
if (allPosts.length === 0) {
|
||||
console.error("Error: No posts found. Import some data first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Calculate date range based on period
|
||||
const now = new Date();
|
||||
let fromDate = new Date(0); // Beginning of time for "all"
|
||||
|
||||
if (period === "week") {
|
||||
fromDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
} else if (period === "month") {
|
||||
fromDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
} else if (period === "quarter") {
|
||||
fromDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
const fromDateStr = fromDate.toISOString().split("T")[0];
|
||||
|
||||
// Filter posts by period
|
||||
const filteredPosts = allPosts.filter(
|
||||
(post) => post.publishedDate >= fromDateStr
|
||||
);
|
||||
|
||||
if (filteredPosts.length === 0) {
|
||||
console.error(`Error: No posts found in the ${period} period.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
const values = filteredPosts.map((post) => post.metrics[metric]);
|
||||
const avg = mean(values);
|
||||
const stdDev = standardDeviation(values);
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
|
||||
console.log("\nTrend Analysis");
|
||||
console.log("═════════════════════════════════════");
|
||||
console.log(`Metric: ${metric}`);
|
||||
console.log(`Period: ${period}`);
|
||||
console.log(`Posts analyzed: ${filteredPosts.length}`);
|
||||
console.log(`Date range: ${filteredPosts[filteredPosts.length - 1].publishedDate} to ${filteredPosts[0].publishedDate}`);
|
||||
console.log();
|
||||
|
||||
console.log("Statistics");
|
||||
console.log("─────────────────────────────────────");
|
||||
console.log(`Mean: ${avg.toFixed(2)}`);
|
||||
console.log(`Std deviation: ${stdDev.toFixed(2)}`);
|
||||
console.log(`Min: ${min.toFixed(2)}`);
|
||||
console.log(`Max: ${max.toFixed(2)}`);
|
||||
console.log();
|
||||
|
||||
// Generate alerts
|
||||
const alerts = detectAlerts(filteredPosts, metric);
|
||||
|
||||
if (alerts.length > 0) {
|
||||
console.log("Alerts");
|
||||
console.log("─────────────────────────────────────");
|
||||
for (const alert of alerts) {
|
||||
const icon = alert.severity === "critical" ? "🔴" : alert.severity === "warning" ? "⚠️" : "ℹ️";
|
||||
console.log(`${icon} [${alert.severity.toUpperCase()}] ${alert.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log("No anomalies detected in this period.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error analyzing trends: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const root = getAnalyticsRoot();
|
||||
ensureDirectories(root);
|
||||
|
||||
switch (command) {
|
||||
case "import":
|
||||
await handleImport(root, args);
|
||||
break;
|
||||
case "report":
|
||||
await handleReport(root, args);
|
||||
break;
|
||||
case "trends":
|
||||
await handleTrends(root, args);
|
||||
break;
|
||||
default:
|
||||
printUsage();
|
||||
process.exit(command ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err instanceof Error ? err.message : String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue