From 1a8cc1942c81b0e652b3f1dda2fc737747ec342b Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Wed, 8 Apr 2026 06:16:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(linkedin-thought-leadership):=20v1.1.0=20?= =?UTF-8?q?=E2=80=94=20Q2=202026=20feature=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../.claude-plugin/plugin.json | 2 +- .../linkedin-thought-leadership/CHANGELOG.md | 23 +++ plugins/linkedin-thought-leadership/CLAUDE.md | 6 +- plugins/linkedin-thought-leadership/README.md | 66 +++---- .../linkedin-thought-leadership/ROADMAP.md | 20 +- .../agents/content-planner.md | 2 +- .../agents/network-builder.md | 2 +- .../agents/trend-spotter.md | 2 +- .../assets/examples/high-engagement-posts.md | 149 +++++++++++--- .../voice-samples/authentic-voice-samples.md | 155 +++++++++------ .../commands/carousel.md | 138 +++++++++++++ .../commands/linkedin.md | 23 +-- .../commands/onboarding.md | 182 ++++++++++++++++++ .../commands/post.md | 14 +- .../commands/quick.md | 11 -- .../commands/react.md | 103 +++++++++- .../commands/report.md | 44 ++++- .../hooks/prompts/post-creation-automation.md | 9 +- .../hooks/prompts/state-update-reminder.md | 17 +- .../hooks/prompts/voice-guardian.md | 42 +++- .../scripts/__tests__/week-rollover.test.mjs | 102 ++++++++++ .../hooks/scripts/session-start.mjs | 26 +-- .../hooks/scripts/week-rollover.mjs | 49 +++++ .../references/thought-leadership-angles.md | 82 ++++++++ .../scripts/analytics/src/cli.ts | 136 +++++++++++++ .../scripts/analytics/src/models/types.ts | 49 +++++ .../scripts/analytics/src/reports/heatmap.ts | 85 ++++++++ .../scripts/analytics/src/reports/monthly.ts | 117 +++++++++++ .../scripts/analytics/src/utils/storage.ts | 40 +++- .../scripts/analytics/tests/heatmap.test.ts | 113 +++++++++++ .../scripts/analytics/tests/monthly.test.ts | 135 +++++++++++++ .../linkedin-thought-leadership/SKILL.md | 2 + .../skills/linkedin-voice/SKILL.md | 2 +- 33 files changed, 1719 insertions(+), 229 deletions(-) create mode 100644 plugins/linkedin-thought-leadership/commands/carousel.md create mode 100644 plugins/linkedin-thought-leadership/commands/onboarding.md create mode 100644 plugins/linkedin-thought-leadership/hooks/scripts/__tests__/week-rollover.test.mjs create mode 100644 plugins/linkedin-thought-leadership/hooks/scripts/week-rollover.mjs create mode 100644 plugins/linkedin-thought-leadership/scripts/analytics/src/reports/heatmap.ts create mode 100644 plugins/linkedin-thought-leadership/scripts/analytics/src/reports/monthly.ts create mode 100644 plugins/linkedin-thought-leadership/scripts/analytics/tests/heatmap.test.ts create mode 100644 plugins/linkedin-thought-leadership/scripts/analytics/tests/monthly.test.ts diff --git a/plugins/linkedin-thought-leadership/.claude-plugin/plugin.json b/plugins/linkedin-thought-leadership/.claude-plugin/plugin.json index 852ccd6..8d18d06 100644 --- a/plugins/linkedin-thought-leadership/.claude-plugin/plugin.json +++ b/plugins/linkedin-thought-leadership/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "linkedin-thought-leadership", - "version": "1.0.0", + "version": "1.1.0", "description": "Build LinkedIn thought leadership with algorithmic understanding, strategic consistency, and authentic engagement. Updated for the January 2026 360Brew algorithm change.", "author": { "name": "Kjell Tore Guttormsen" diff --git a/plugins/linkedin-thought-leadership/CHANGELOG.md b/plugins/linkedin-thought-leadership/CHANGELOG.md index d1f7154..8283ac1 100644 --- a/plugins/linkedin-thought-leadership/CHANGELOG.md +++ b/plugins/linkedin-thought-leadership/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2026-04-08 + +### Summary +Q2 2026 feature release. 9 improvements across onboarding, content quality, and analytics pipeline. + +### Added +- **`/linkedin:onboarding`** — multi-step onboarding wizard: profile → setup → first-post as one guided flow +- **`/linkedin:carousel`** — structured multi-slide carousel generator with 5 templates and design specs +- **Voice drift scoring** — 6-dimension rubric (sentence structure, word choice, openings, storytelling, tone, formatting) with AUTHENTIC/CAUTION/ALERT/REWRITE verdicts in voice-guardian hook +- **Industry angle variants** — 48 concrete variants (6 industries × 8 angles) in thought-leadership-angles reference +- **Multi-URL comparison** — `/linkedin:react` now supports 2-3 URL synthesis with contrarian and pattern analysis angles +- **Day-of-week heatmap** — `heatmap` CLI command and `HeatmapReport` type in analytics pipeline +- **Month-over-month reports** — `report --month YYYY-MM` CLI command with MoM deltas, weekly breakdown, top performers +- **Automated week-rollover** — session-start hook now writes `posts_this_week: 0` and updates `current_week` on ISO week change +- **Collected Post Samples** — Stop hook passively accumulates published posts in voice-samples file for drift scoring + +### Changed +- **README Quick Start** — replaced 4-step manual flow with single `/linkedin:onboarding` entry point +- **`/linkedin:report`** — Step 2 now offers report type choice (weekly/monthly/heatmap) +- **`/linkedin:post`** — Step 2 shows industry-specific angles when user-profile has industry set; Step 3 redirects to carousel when appropriate +- **`/linkedin` router** — added onboarding and carousel to menus and direct routing +- **Command count** — 25 → 27 (onboarding, carousel) + ## [1.0.0] - 2026-04-07 ### Summary diff --git a/plugins/linkedin-thought-leadership/CLAUDE.md b/plugins/linkedin-thought-leadership/CLAUDE.md index 5df77ed..2d67c5e 100644 --- a/plugins/linkedin-thought-leadership/CLAUDE.md +++ b/plugins/linkedin-thought-leadership/CLAUDE.md @@ -1,4 +1,4 @@ -# LinkedIn Thought Leadership Plugin (v1.0.0) +# LinkedIn Thought Leadership Plugin (v1.1.0) Build LinkedIn thought leadership with algorithmic understanding, strategic consistency, and authentic engagement. January 2026 360Brew algorithm update integrated. @@ -27,11 +27,12 @@ Build LinkedIn thought leadership with algorithmic understanding, strategic cons **Hook editing:** Edit `hooks/hooks.template.json` + `hooks/prompts/*.md`, then run `python3 hooks/scripts/compile-hooks.py`. Do not edit `hooks.json` directly. Prompts are loaded at runtime by gatekeeper scripts; the compile step is only needed when adding `type: prompt` hooks. -## Commands (25) +## Commands (27) | Command | Purpose | |---------|---------| | `/linkedin` | Router — status line + command menu | +| `/linkedin:onboarding` | Multi-step onboarding wizard (profile → setup → first-post) | | `/linkedin:first-post` | First-post accelerator (10 min) | | `/linkedin:setup` | Guided personalization setup | | `/linkedin:react` | URL-to-post pipeline | @@ -42,6 +43,7 @@ Build LinkedIn thought leadership with algorithmic understanding, strategic cons | `/linkedin:batch` | Create a full week of content | | `/linkedin:calendar` | View/manage post scheduling queue | | `/linkedin:publish` | Mark scheduled posts as published | +| `/linkedin:carousel` | Structured multi-slide carousel generator | | `/linkedin:video` | Video script generator (30s-2min) | | `/linkedin:multiplatform` | Adapt content for other platforms | | `/linkedin:analyze` | Content/performance analysis | diff --git a/plugins/linkedin-thought-leadership/README.md b/plugins/linkedin-thought-leadership/README.md index ba349ed..35393c6 100644 --- a/plugins/linkedin-thought-leadership/README.md +++ b/plugins/linkedin-thought-leadership/README.md @@ -4,15 +4,15 @@ *Built for my own Claude Code workflow and shared openly for anyone who finds it useful. This is a solo project — bug reports and feature requests are welcome, but pull requests are not accepted.* -![Version](https://img.shields.io/badge/version-1.0.0-blue) +![Version](https://img.shields.io/badge/version-1.1.0-blue) ![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple) -![Commands](https://img.shields.io/badge/commands-25-green) +![Commands](https://img.shields.io/badge/commands-27-green) ![Agents](https://img.shields.io/badge/agents-16-orange) ![Hooks](https://img.shields.io/badge/hooks-9-red) ![Reference Docs](https://img.shields.io/badge/reference_docs-24-teal) ![License](https://img.shields.io/badge/license-MIT-lightgrey) -A comprehensive Claude Code plugin that turns LinkedIn from a chore into a system. It covers the full content lifecycle — from ideation and drafting through publishing, analytics, and growth strategy — with 25 slash commands, 16 specialized agents, 9 automated hooks, and a 24-document knowledge base grounded in LinkedIn's actual algorithm signals. Updated for the January 2026 **360Brew** algorithm change, where LinkedIn now validates your profile before distributing content. +A comprehensive Claude Code plugin that turns LinkedIn from a chore into a system. It covers the full content lifecycle — from ideation and drafting through publishing, analytics, and growth strategy — with 27 slash commands, 16 specialized agents, 9 automated hooks, and a 24-document knowledge base grounded in LinkedIn's actual algorithm signals. Updated for the January 2026 **360Brew** algorithm change, where LinkedIn now validates your profile before distributing content. --- @@ -82,55 +82,39 @@ Or add to your `~/.claude/settings.json`: } ``` -### First Conversation +### Get Started (5 minutes) -#### 1. Optimize Your Profile (Critical) - -With the 360Brew update, profile optimization is no longer optional — LinkedIn validates your profile before distributing content: +Run the onboarding wizard — it walks you through profile, setup, and your first post in one flow: ``` -/linkedin:profile +/linkedin:onboarding ``` -#### 2. Personalize the Plugin +The wizard handles everything: 360Brew profile checklist, voice and user profile setup, and a guided first post. -```bash -cp config/user-profile.template.md config/user-profile.local.md -# Edit with your name, expertise, audience, voice, and goals -``` +### Already Set Up? -Then run the guided setup to populate all asset templates: - -``` -/linkedin:setup -``` - -#### 3. Create Your First Post - -``` -/linkedin:post -> I want to write about how AI is changing public sector procurement -``` - -For a faster start, try the 5-minute quick post: - -``` -/linkedin:quick -``` - -#### 4. Explore All Commands - -``` -/linkedin -``` - -The router shows your current posting status (streak, weekly progress) and lists all available commands. +| Goal | Command | +|------|---------| +| Write a post | `/linkedin:post` | +| Quick 5-min post | `/linkedin:quick` | +| React to an article | `/linkedin:react` | +| View your stats | `/linkedin:report` | +| See all commands | `/linkedin` | --- ## Commands -All 25 commands use colon notation: `/linkedin:post`, `/linkedin:quick`, etc. +All 26 commands use colon notation: `/linkedin:post`, `/linkedin:quick`, etc. + +### Onboarding + +| Command | Description | +|---------|-------------| +| `/linkedin:onboarding` | Multi-step onboarding wizard — guides you through profile optimization, plugin personalization, and your first post in one flow. | +| `/linkedin:first-post` | First-post accelerator — zero to published in 10 minutes with guided hand-holding. | +| `/linkedin:setup` | Guided setup to populate empty asset templates with your real voice, case studies, and audience data. | ### Content Creation @@ -146,7 +130,6 @@ All 25 commands use colon notation: `/linkedin:post`, `/linkedin:quick`, etc. | `/linkedin:video` | Video script generator for 30s, 60s, 90s, or 2-minute LinkedIn videos with pacing and visual cues. | | `/linkedin:multiplatform` | Adapt LinkedIn content for Twitter/X threads, newsletter sections, blog posts, presentation slides, and YouTube scripts. | | `/linkedin:react` | URL-to-post pipeline — paste an article, research paper, or news link and generate a reaction post. | -| `/linkedin:first-post` | First-post accelerator — zero to published in 10 minutes with guided hand-holding. | ### Analytics @@ -518,6 +501,7 @@ Scheduled posts are tracked in `assets/drafts/queue.json`: | Version | Date | Highlights | |---------|------|-----------| +| **1.1.0** | 2026-04-08 | Q2 feature release. 27 commands (+onboarding, +carousel). Week-rollover automation, voice drift scoring, industry content matrix, multi-URL react, day-of-week heatmap, month-over-month reports. | | **1.0.0** | 2026-04-07 | Public release. 25 commands, 16 agents, 9 hooks, 6 skills, 24 reference docs. Agent model tiering (Sonnet/Haiku), all scripts Node.js, comprehensive documentation. | | **0.6.0** | 2026-02-07 | First formal version. 20 commands, 15 agents, 8 hooks, analytics system, 360Brew profile optimization, content matrix system, personalization engine, 20 reference documents. | diff --git a/plugins/linkedin-thought-leadership/ROADMAP.md b/plugins/linkedin-thought-leadership/ROADMAP.md index 9d5fb61..444fef4 100644 --- a/plugins/linkedin-thought-leadership/ROADMAP.md +++ b/plugins/linkedin-thought-leadership/ROADMAP.md @@ -1,6 +1,6 @@ # LinkedIn Thought Leadership Plugin — Roadmap -**Current version:** v1.0.0 (April 2026) +**Current version:** v1.1.0 (April 2026) **Scope:** Planned improvements through Q4 2026 Items organized by quarter and track. Priority = Impact / Effort (High/Medium/Low). Items within each quarter ordered by priority. @@ -16,23 +16,23 @@ Items organized by quarter and track. Priority = Impact / Effort (High/Medium/Lo - [x] Condensed getting-started menu for zero-post users in `/linkedin` router - [x] Readiness check in `/linkedin:post` and `/linkedin:quick` for unpersonalized state - [x] Inline 5x5x5 engagement ritual explanation -- [ ] `/linkedin:onboarding` — dedicated multi-step onboarding command that guides profile → setup → first-post as one flow -- [ ] README Quick Start refinement — 5-minute getting-started path with screenshots +- [x] `/linkedin:onboarding` — dedicated multi-step onboarding command that guides profile → setup → first-post as one flow +- [x] README Quick Start refinement — 5-minute getting-started path with single `/linkedin:onboarding` entry point ### Content Quality **Priority: High** | **Effort: Medium** -- [ ] Enhanced voice-trainer agent: automatic drift scoring on every post draft (compare against voice samples) -- [ ] Content Matrix improvements: add industry-specific angle variants -- [ ] Carousel post support: structured multi-slide content generation with visual layout guidance -- [ ] `/linkedin:react` enhancement: multi-URL comparison posts (compare 2-3 articles) +- [x] Enhanced voice-trainer agent: automatic drift scoring on every post draft (compare against voice samples) +- [x] Content Matrix improvements: add industry-specific angle variants +- [x] Carousel post support: structured multi-slide content generation with visual layout guidance +- [x] `/linkedin:react` enhancement: multi-URL comparison posts (compare 2-3 articles) ### Analytics Pipeline **Priority: Medium** | **Effort: Medium** -- [ ] Automated week-rollover: session-start hook resets `posts_this_week` and updates `current_week` on week change (currently warn-only) -- [ ] Post-level heatmap generation: day-of-week x time-of-day performance matrix from imported CSV data -- [ ] `/linkedin:report` month-over-month comparison view +- [x] Automated week-rollover: session-start hook resets `posts_this_week` and updates `current_week` on week change +- [x] Post-level heatmap generation: day-of-week performance matrix from imported CSV data (time-of-day not available in CSV export) +- [x] `/linkedin:report` month-over-month comparison view --- diff --git a/plugins/linkedin-thought-leadership/agents/content-planner.md b/plugins/linkedin-thought-leadership/agents/content-planner.md index 93e9564..8b7ed8e 100644 --- a/plugins/linkedin-thought-leadership/agents/content-planner.md +++ b/plugins/linkedin-thought-leadership/agents/content-planner.md @@ -183,7 +183,7 @@ APRIL MAY - Microsoft Build (typically May) → AI announcements - - [National/regional holiday] → Cultural content + - 17. mai (Norwegian National Day) → Cultural content - End of spring conference season wrap-ups JUNE diff --git a/plugins/linkedin-thought-leadership/agents/network-builder.md b/plugins/linkedin-thought-leadership/agents/network-builder.md index dd64c3a..1116fe9 100644 --- a/plugins/linkedin-thought-leadership/agents/network-builder.md +++ b/plugins/linkedin-thought-leadership/agents/network-builder.md @@ -5,7 +5,7 @@ description: | who to engage with, tracks engagement history, and guides the 5x5x5 method with specific people and posts to target. Includes connection request templates (300-char limit), collaboration pitch templates, follow-up sequences (day 1-30), and connection scoring - criteria. + criteria. Inherits DM template functionality from cancelled UPYOU-2078. Use when the user says: - "who should I connect with", "networking strategy", "build my network" diff --git a/plugins/linkedin-thought-leadership/agents/trend-spotter.md b/plugins/linkedin-thought-leadership/agents/trend-spotter.md index 866aaaf..d0e1f15 100644 --- a/plugins/linkedin-thought-leadership/agents/trend-spotter.md +++ b/plugins/linkedin-thought-leadership/agents/trend-spotter.md @@ -52,7 +52,7 @@ Before scanning, load the user's content pillars and expertise areas: ### Tier 1: Breaking News (daily, respond within 24-48h) - **OpenAI**, **Anthropic**, **Microsoft AI**, **Google AI** -- blog posts and announcements -- **EU/[your region's] government** AI regulatory decisions +- **EU/Norwegian government** AI regulatory decisions ### Tier 2: Analysis & Research (2-3x/week, post within a week) diff --git a/plugins/linkedin-thought-leadership/assets/examples/high-engagement-posts.md b/plugins/linkedin-thought-leadership/assets/examples/high-engagement-posts.md index 7df25d9..147f811 100644 --- a/plugins/linkedin-thought-leadership/assets/examples/high-engagement-posts.md +++ b/plugins/linkedin-thought-leadership/assets/examples/high-engagement-posts.md @@ -14,58 +14,145 @@ Claude will study these to understand your successful patterns and apply them to --- - - ## Patterns Across All High-Performing Posts **Common Elements:** -- [ ] Specific numbers in hook -- [ ] Personal story structure (I did X, here's what happened) -- [ ] Concrete timeline and details -- [ ] Strong CTA -- [ ] Optimal timing +- [x] Specific numbers in hook (10,000 lines, 250 people) +- [x] Personal story structure (I did X, here's what happened) +- [x] Concrete timeline and details +- [ ] Strong CTA (not yet tested) +- [ ] Optimal timing (not yet tested) **Audience Preferences (What YOUR Audience Responds To):** -- Format: [Discover from your data] -- Length: [Your typical length] -- Tone: [Your tone pattern] -- CTAs: [What works for your audience?] +- Format: Story-based posts with concrete details +- Length: ~2,100 characters (slightly over optimal 1,800) +- Tone: Professional but personal, showing vulnerability ("I'm not a developer") +- CTAs: Unknown - need to test **Topics That Resonate:** -1. [Add after 3+ posts] +1. AI-assisted coding / Vibe coding 2. [More data needed] 3. [More data needed] **Best Posting Times (Based on YOUR Data):** -- Primary: [Test and record] -- Secondary: [Test and record] -- **Avoid:** [Based on your data] +- Primary: Unknown - need to test 08:00 CET +- Secondary: Unknown - need to test +- **Avoid:** After 21:00 (confirmed by Ralph Wiggum failure) + +## Update Log + +- 2026-01-24: Added Ralph Wiggum post as baseline reference. Note: Post had good engagement rate (4.98%) despite multiple mistakes, suggesting content quality is strong. Focus on fixing timing, CTA, and link placement for next posts. diff --git a/plugins/linkedin-thought-leadership/assets/voice-samples/authentic-voice-samples.md b/plugins/linkedin-thought-leadership/assets/voice-samples/authentic-voice-samples.md index deced82..7f45d07 100644 --- a/plugins/linkedin-thought-leadership/assets/voice-samples/authentic-voice-samples.md +++ b/plugins/linkedin-thought-leadership/assets/voice-samples/authentic-voice-samples.md @@ -1,71 +1,100 @@ -# Authentic Voice Samples - [Your Name] +# Authentic Voice Samples - Kjell Tore Guttormsen -These guidelines help Claude understand and replicate [Your Name]'s natural writing style for LinkedIn content. +These guidelines help Claude understand and replicate Kjell Tore's natural writing style for LinkedIn content. ## Voice Profile Summary -Fill in this section with your own writing characteristics. Run `/linkedin:setup` to build your voice profile interactively, or edit this file directly. +Kjell Tore does not have traditional writing samples to share. Instead, his voice is defined by the following characteristics which Claude should internalize and apply consistently. --- ## Core Voice Characteristics - +### 1. Solution-Oriented Mindset +- Every problem is presented as an opportunity +- Never complains without offering a path forward +- Focuses on "what can be done" rather than "what went wrong" +- Sees challenges as interesting puzzles to solve -### 1. [Your Primary Trait] -- [Describe how this trait shows up in your writing] -- [What makes your approach distinctive?] +### 2. Factual Grounding +- Statements are based on facts, not assumptions +- If uncertain, acknowledges uncertainty openly +- Prefers data and evidence over opinions +- Avoids speculation presented as fact -### 2. [Your Secondary Trait] -- [Describe how this trait shows up in your writing] +### 3. Non-Judgmental Tone +- Observes and explains without criticizing others +- Builds up, never tears down +- Avoids negative commentary about people, companies, or decisions +- When discussing alternatives, frames as "different approaches" not "better/worse" -### 3. [Your Third Trait] -- [Describe how this trait shows up in your writing] +### 4. Curiosity and Openness +- Genuinely interested in learning new things +- Open to new ideas and approaches +- Asks questions to understand, not to challenge +- Embraces "I don't know" as a starting point for exploration - +### 5. Storytelling Approach +- Uses narrative techniques to make points memorable +- Varies storytelling patterns based on content: + - Hero's journey (transformation stories) + - Problem-solution (practical content) + - Before-after (showing change/improvement) + - Discovery narrative (learning something new) + - Day-in-the-life (practical application) +- Shows rather than tells + +### 6. Actionable Conclusions +- Ends with something the reader can do +- The more actionable, the better +- If no clear action, provides a clear summary/takeaway +- Never ends on a vague note --- ## Cross-Sample Analysis -### Do's (Things that sound like [Your Name]) +### Do's (Things that sound like Kjell Tore) - +- ✅ Start with stories or concrete examples before explaining concepts +- ✅ Use clear, accessible language even for technical topics +- ✅ Explain technical concepts thoroughly - assume intelligence, not knowledge +- ✅ Show rather than tell - demonstrate with examples +- ✅ End with actionable takeaways - what can the reader do NOW? +- ✅ Vary storytelling techniques based on the content +- ✅ Be genuinely helpful and supportive +- ✅ Acknowledge complexity before simplifying +- ✅ Use transitions like "What I've learned is..." to share insights +- ✅ Frame discoveries as shared learning, not lecturing +- ✅ Keep posts concise - short to medium length (800-1500 characters) -- Start with stories or concrete examples before explaining concepts -- Use clear, accessible language even for technical topics -- Explain technical concepts thoroughly - assume intelligence, not knowledge -- Show rather than tell - demonstrate with examples -- End with actionable takeaways - what can the reader do NOW? -- Vary storytelling techniques based on the content -- Be genuinely helpful and supportive -- Acknowledge complexity before simplifying -- Frame discoveries as shared learning, not lecturing -- Keep posts concise - short to medium length (800-1500 characters) +### Don'ts (Things Kjell Tore would NEVER say) -### Don'ts (Things [Your Name] would NEVER say) - - - -- Don't use buzzwords: "game-changer", "leverage", "synergy", "disrupt", "revolutionize" -- Don't criticize people, companies, or decisions -- Don't make assumptions without facts -- Don't write overly long posts (stay under 1500 characters for posts) -- Don't use more than 1-2 emojis per post -- Don't start with "Let's dive deep into..." -- Don't use excessive exclamation marks -- Don't use generic motivational phrases -- Don't be preachy or lecture the reader +- ❌ Don't use buzzwords: "game-changer", "leverage", "synergy", "disrupt", "revolutionize" +- ❌ Don't criticize people, companies, or decisions +- ❌ Don't use self-deprecating humor +- ❌ Don't make assumptions without facts +- ❌ Don't write overly long posts (stay under 1500 characters for posts) +- ❌ Don't use more than 1-2 emojis per post +- ❌ Don't discuss politics, religion, or personal matters +- ❌ Don't use em dashes (—) - use hyphens or alternatives instead +- ❌ Don't start with "Let's dive deep into..." +- ❌ Don't use excessive exclamation marks!!! +- ❌ Don't use generic motivational phrases +- ❌ Don't be preachy or lecture the reader +- ❌ Don't use "we" when you mean "I" (be direct about personal experience) --- ## Signature Phrases - +Use these naturally when appropriate - don't force them: -- "[Your phrase 1]" -- "[Your phrase 2]" -- "[Your phrase 3]" +- "Let me show you..." +- "What I've learned is..." +- "Here is the secret to..." + +These phrases signal a transition to insight or demonstration. Use them to introduce key points or revelations. --- @@ -73,11 +102,11 @@ Fill in this section with your own writing characteristics. Run `/linkedin:setup ### Technical Terms - How to Handle - - -- **[Term 1]:** [How to explain/use it] -- **[Term 2]:** [How to explain/use it] -- **[Term 3]:** [How to explain/use it] +- **RAG (Retrieval-Augmented Generation):** Always explain on first use +- **MCP (Model Context Protocol):** Explain what it enables, not just the acronym +- **Copilot Studio:** Can assume some familiarity with Microsoft ecosystem +- **Skills (Claude):** Explain as "reusable instruction sets" or similar +- **Low-code:** Generally understood, but clarify scope if needed **Principle:** Assume intelligence, not knowledge. Explain jargon without being condescending. @@ -100,10 +129,10 @@ Fill in this section with your own writing characteristics. Run `/linkedin:setup ## Humor and Personality -- **Humor style:** [Describe your humor approach - absent, dry, observational, etc.] -- **Self-deprecation:** [Your preference] -- **Cultural references:** [Your approach] -- **Analogies:** [What kind of analogies work for your audience?] +- **Humor style:** Mostly absent in professional content. If humor appears, it's observational and gentle - never at anyone's expense +- **Self-deprecation:** Never. Don't undermine your own credibility. +- **Cultural references:** Avoid pop culture references. Stick to professional/work context. +- **Analogies:** Use when helpful for explanation. Prefer technical or universal analogies over sports/culture-specific ones. --- @@ -136,14 +165,14 @@ Match technical depth to the target audience: - ROI and outcomes - Avoid implementation details -### For Practitioners +### For Low-Code Developers - Practical tips and patterns - Step-by-step guidance - Tool-specific insights - Common pitfalls and solutions - Can include some technical detail -### For Technical Experts +### For AI Architects - Technical depth welcome - Architecture patterns - Integration approaches @@ -163,24 +192,36 @@ Match technical depth to the target audience: ## Language Guidelines -- Choose ONE language for all LinkedIn content and stick with it +- **Always English** for all LinkedIn content - Clear, international English accessible to non-native speakers - Avoid idioms that don't translate well internationally - Prefer simple sentence structures for complex ideas +- Never use em dashes (—) - use hyphens, commas, or separate sentences instead --- ## Instructions for Claude -When generating LinkedIn content for [Your Name]: +When generating LinkedIn content for Kjell Tore: -1. **Start with the voice profile** (from this document) +1. **Start with his voice profile** (from this document) 2. **Check the content pillar** - which audience is this for? 3. **Choose appropriate storytelling technique** for the content type 4. **Ensure actionable conclusion** - what can the reader DO? 5. **Verify against Don'ts list** - no buzzwords, no criticism, no assumptions 6. **Keep length in check** - 800-1500 characters for posts -**Priority:** Sound like [Your Name] > Optimize for algorithm +**Priority:** Sound like Kjell Tore > Optimize for algorithm -**Exception:** If a phrase or approach would harm reach (external links, engagement bait), flag it but maintain the voice in everything else. +**Exception:** If a phrase or approach would harm reach (external links, engagement bait), flag it but maintain his voice in everything else. + +--- + +## Update Log + +- 2025-11-30: Initial voice profile created based on interview + +## Collected Post Samples + + + diff --git a/plugins/linkedin-thought-leadership/commands/carousel.md b/plugins/linkedin-thought-leadership/commands/carousel.md new file mode 100644 index 0000000..477eaf1 --- /dev/null +++ b/plugins/linkedin-thought-leadership/commands/carousel.md @@ -0,0 +1,138 @@ +--- +name: linkedin:carousel +description: | + Create a LinkedIn carousel post with structured slide-by-slide content and visual layout guidance. + Carousels have the highest engagement rate (6.6%) on LinkedIn. Guides template selection, + topic definition, and generates copy for each slide plus caption. + Triggers on: "carousel", "slide deck", "pdf post", "swipe post", "multi-slide", + "linkedin carousel", "document post", "create slides". +allowed-tools: + - Read + - AskUserQuestion +--- + +# Carousel Post Generator + +You are a LinkedIn carousel content specialist. Create high-engagement carousel posts with structured slide content and visual layout guidance. + +## Step 0: Load Context + +- Read `~/.claude/linkedin-thought-leadership.local.md` for posting state and expertise areas +- Read `assets/voice-samples/authentic-voice-samples.md` for voice profile +- Check recent posts to avoid topic repetition + +## Step 1: Choose Template + +Read `assets/templates/carousel-templates.md` for the 5 templates. + +Present the options: + +``` +LinkedIn carousels get 6.6% average engagement — highest of all formats. + +Choose a template: + +1. How-To Guide — Teach a process step-by-step (8-10 slides) +2. Listicle / Top N — Curated list of tips, tools, or lessons (8-12 slides) +3. Story / Before-After — Personal narrative with transformation (8-10 slides) +4. Comparison / vs. — Side-by-side analysis of two approaches (8-10 slides) +5. Framework / Mental Model — Present an original framework (8-10 slides) +``` + +Use AskUserQuestion for selection. + +## Step 2: Define Topic and Audience + +Ask: +1. "What's the core topic or insight for this carousel?" +2. "Who is the primary audience? (e.g., developers, managers, executives)" + +If the user's expertise areas are set in the state file, suggest topics aligned with their pillars. + +## Step 3: Generate Slide Content + +Using the selected template structure from `carousel-templates.md`, generate content for each slide. + +**Output format for each slide:** + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +SLIDE [N] of [TOTAL] — [Purpose from template] +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +HEADER: +[Bold headline text — max 8 words] + +BODY: +[Line 1 — max 50 chars] +[Line 2 — max 50 chars] +[Line 3 — max 50 chars] +[Line 4 — max 50 chars (optional)] +[Line 5 — max 50 chars (optional)] + +VISUAL NOTE: +[Layout suggestion: e.g., "Icon: lightbulb left of header", +"Before/After split layout", "Numbered list with accent color", +"Summary table with checkmarks"] +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +**Slide content rules:** +- Max 5-7 lines of body text per slide (mobile readability) +- One idea per slide — if it needs two points, it needs two slides +- Use the template's formula for each slide type (cover, step, item, CTA, etc.) +- Headlines in sentence case, not ALL CAPS +- Include the template-specific patterns (Pro tip, Before/After, Winner, etc.) + +## Step 4: Generate Caption + +Generate a caption following the carousel caption structure from the template file: + +1. **Hook** (first line): Question, bold claim, or surprising stat — 110-140 chars +2. **Context** (1-2 lines): Why this matters to the audience +3. **Swipe prompt**: Reference a specific slide to create curiosity +4. **Engagement CTA**: Question that invites comments +5. **Hashtags**: 3-4 maximum + +Target: 300-500 characters total. + +Match the user's voice profile — check against avoid-list and tone markers. + +## Step 5: Quality Check + +Run against the Carousel Quality Checklist from `carousel-templates.md`: + +- [ ] Cover slide has a clear promise or question +- [ ] Each slide has one point (not multiple ideas) +- [ ] Text is readable on mobile (keep lines short) +- [ ] 8-10 slides total +- [ ] Last slide has a clear CTA +- [ ] Caption hooks attention and prompts swipe +- [ ] Consistent structure across all slides + +If any item fails, fix before presenting. + +## Step 6: Present Complete Deck + +Show all slides in order, then the caption, then design guidance: + +``` +DESIGN GUIDE +━━━━━━━━━━━━ +Dimensions: 1080 × 1350 px (4:5 portrait) +Font: Sans-serif, 24pt+ body, 36pt+ headlines +Colors: Pick 3 — background, text, accent +Export: PDF format, under 100 MB +Tools: Canva, PowerPoint, Figma, or Keynote + +Create one slide per page using the content above. +Export as PDF and upload directly to LinkedIn. +``` + +Use AskUserQuestion: "Want to refine any slides, or is this ready for design?" + +## Step 7: State Update + +If the user confirms the carousel is ready: +- Note in state file: topic, format=carousel, slide count +- Suggest: "After publishing, run the 5x5x5 engagement method for maximum reach." diff --git a/plugins/linkedin-thought-leadership/commands/linkedin.md b/plugins/linkedin-thought-leadership/commands/linkedin.md index 7985571..8fad3da 100644 --- a/plugins/linkedin-thought-leadership/commands/linkedin.md +++ b/plugins/linkedin-thought-leadership/commands/linkedin.md @@ -27,23 +27,6 @@ The follower segment only appears if `follower_count > 0` in the state file. If the state file doesn't exist, show: "No LinkedIn state tracked yet. State tracking starts when you create your first post." -## New User Detection - -After reading the state file, check if `first_post_date` is null/empty AND `posts_this_week` is 0. If so, this is a new user. Show a condensed getting-started menu INSTEAD of the full command list: - -**You haven't posted yet! Here's where to start:** - -| # | Action | Time | -|---|--------|------| -| 0 | **Profile audit** — optimize for 360Brew algorithm | 10 min | -| 1 | **Personalize** — set up your voice, audience, and goals | 15 min | -| 2 | **First post** — guided creation with hand-holding | 10 min | -| 3 | **Show all commands** — I know what I'm doing | - -Use AskUserQuestion with these 4 options. Route 0 → `/linkedin:profile`, 1 → `/linkedin:setup`, 2 → `/linkedin:first-post`, 3 → continue to the full command list below. - -**Skip this section entirely if `first_post_date` is set or `posts_this_week` > 0.** Proceed to Upcoming Posts and the full command list. - ## Upcoming Posts After the status line, show upcoming scheduled posts from the queue: @@ -81,6 +64,7 @@ Present these options to the user: | Command | Purpose | |---------|---------| +| `/linkedin:onboarding` | Full onboarding wizard — profile, setup, and first post in one flow | | `/linkedin:first-post` | First-post accelerator — zero to published in under 10 minutes | | `/linkedin:setup` | Guided setup to populate empty asset templates with your real voice, case studies, and audience data | @@ -93,6 +77,7 @@ Present these options to the user: | `/linkedin:quick` | Fast 5-minute post using the 3-line formula | | `/linkedin:templates` | Browse and apply proven post templates | | `/linkedin:pipeline` | Full end-to-end workflow from idea to post-publish analysis | +| `/linkedin:carousel` | Create structured multi-slide carousel with visual layout guidance | | `/linkedin:video` | Create video scripts with hook, body, CTA, captions, and thumbnail suggestions | | `/linkedin:batch` | Create a full week of content in one session | | `/linkedin:calendar` | View and manage your post scheduling queue | @@ -133,7 +118,7 @@ Use AskUserQuestion to ask: **What would you like to do?** -0. **First post** — Never posted? Start here (10 min) +0. **Onboarding wizard** — Just installed? Full guided flow: profile → setup → first post 1. **Setup & personalize** — Guided setup to populate voice, case studies, frameworks, and audience data 2. **Create a post** — Full post workflow with angle selection 3. **React to a URL** — Turn an article/news into a post @@ -170,6 +155,7 @@ If the user already has content they want to turn into a post: ## Direct Routing If the user's intent is clear from context: +- Mentions "onboarding" or "just installed" or "walk me through" or "setup wizard" or "start from scratch" → Route to `/linkedin:onboarding` - Mentions "first post" or "never posted" or "get started" or "new to linkedin" or "help me start" → Route to `/linkedin:first-post` - Mentions "setup" or "personalize" or "templates empty" or "score" or "fill in assets" or "configure plugin" → Route to `/linkedin:setup` - Mentions "react" or "this article" or "this url" or "turn this into" or "share this news" → Route to `/linkedin:react` @@ -182,6 +168,7 @@ If the user's intent is clear from context: - Mentions "profile" or "360Brew" → Route to `/linkedin:profile` - Mentions "not working" or "low reach" → Route to `/linkedin:analyze` - Mentions "strategy" or "growth plan" → Route to `/linkedin:strategy` +- Mentions "carousel" or "slides" or "slide deck" or "pdf post" or "swipe" or "document post" → Route to `/linkedin:carousel` - Mentions "template" → Route to `/linkedin:templates` - Mentions "audit" or "review strategy" → Route to `/linkedin:audit` - Mentions "authority" or "signature content" → Route to `/linkedin:authority` diff --git a/plugins/linkedin-thought-leadership/commands/onboarding.md b/plugins/linkedin-thought-leadership/commands/onboarding.md new file mode 100644 index 0000000..f28bfcf --- /dev/null +++ b/plugins/linkedin-thought-leadership/commands/onboarding.md @@ -0,0 +1,182 @@ +--- +name: linkedin:onboarding +description: | + Multi-step onboarding wizard that guides new users through profile → setup → first-post + as one cohesive flow. Designed for users who have just installed the plugin and want a + single guided path instead of navigating 25 commands on their own. + Triggers on: "onboarding", "get started", "new user", "setup wizard", "start from scratch", + "just installed", "how do I start", "walk me through", "linkedin onboarding". +allowed-tools: + - Read + - Bash + - AskUserQuestion +--- + +# LinkedIn Onboarding Wizard + +You are a LinkedIn thought leadership onboarding guide. Walk the user through profile optimization, plugin personalization, and their first post — all in one session. + +## Step 0: Load Context and Check State + +Read `~/.claude/linkedin-thought-leadership.local.md` for current state. + +**Already onboarded check:** If `first_post_date` is set (not null) AND personalization score > 50: +- Show: "You've already completed onboarding (first post: [date], personalization: [score]%)." +- Use AskUserQuestion: "Would you like to re-run a specific phase?" + 1. Re-optimize profile (360Brew) → jump to Phase 1 + 2. Improve personalization → jump to Phase 2 + 3. Create another post → suggest `/linkedin:post` or `/linkedin:quick` + 4. Exit + +If not already onboarded, continue to Phase 1. + +## Phase 1: Profile Optimization (360Brew) + +``` +╔═══════════════════════════════════════╗ +║ ONBOARDING — Phase 1 of 3: Profile ║ +╚═══════════════════════════════════════╝ +``` + +Explain briefly: +- LinkedIn's 360Brew algorithm (January 2026) validates your profile BEFORE distributing your content +- A weak profile means even great posts get suppressed +- This takes 5 minutes and has outsized impact on everything else + +Use AskUserQuestion: +1. **Guide me through profile optimization** — I want the full 360Brew checklist +2. **Already optimized** — I've already done this, skip ahead +3. **Do it later** — Skip for now, I'll run `/linkedin:profile` later + +**If option 1:** Walk through the core 360Brew checklist (condensed from `/linkedin:profile`): +- [ ] Professional headshot (face visible, good lighting) +- [ ] Headline with expertise + value prop (not just job title) +- [ ] About section with story arc + CTA (not a resume) +- [ ] Banner image related to expertise +- [ ] Featured section with best content or lead magnet +- [ ] Creator mode ON (if available) + +After each item, ask if done or needs to skip. Don't block — mark skipped items as "recommended later." + +**If option 2 or 3:** Move to Phase 2. + +## Phase 2: Plugin Personalization + +``` +╔═════════════════════════════════════════════╗ +║ ONBOARDING — Phase 2 of 3: Personalization ║ +╚═════════════════════════════════════════════╝ +``` + +Calculate personalization score: +```bash +node --input-type=module -e " +import { calculateScore } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/personalization-score.mjs'; +const result = calculateScore('${CLAUDE_PLUGIN_ROOT}'); +console.log(JSON.stringify(result)); +" +``` + +Show the score dashboard: +``` +Personalization Score: [XX]% + +Category Weight Status +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Voice samples 25 [✓ Done / ○ Empty] +User profile 20 [✓ Done / ○ Empty] +Case studies 15 [✓ Done / ○ Empty] +Frameworks 10 [✓ Done / ○ Empty] +High-eng. posts 10 [✓ Done / ○ Empty] +Demographics 8 [✓ Done / ○ Empty] +Engagement patterns 7 [✓ Done / ○ Empty] +Post templates 5 [✓ Done / ○ Empty] +``` + +Identify the **top 2 incomplete categories by weight** and guide through those: + +**Priority setup (2 categories only — keep it focused):** + +Use AskUserQuestion: +1. **Set up voice profile** (weight: 25) — 5 questions about your writing style, or paste 3 examples +2. **Set up user profile** (weight: 20) — Your name, industry, expertise areas, audience +3. **Both** — Do voice + user profile now +4. **Skip for now** — I'll run `/linkedin:setup` later for the full setup + +**If voice selected:** Run a quick 5-question voice interview: +1. "How would you describe your communication style in one sentence?" +2. "What words or phrases do you naturally use?" (give examples) +3. "What tone turns you off in LinkedIn content?" +4. "Paste a paragraph you've written that sounds like YOU (email, doc, anything)" +5. "Any words or phrases you'd NEVER use?" + +Save responses to `assets/voice-samples/authentic-voice-samples.md` under a new section `## Quick Voice Interview` (append, don't overwrite existing content). + +**If user profile selected:** Ask for: +1. Full name +2. Industry +3. Job title / role +4. 3-5 expertise areas (these become your content pillars) +5. Target audience description + +Save to `config/user-profile.local.md`. + +After setup, recalculate and show updated score. + +## Phase 3: First Post + +``` +╔═══════════════════════════════════════════╗ +║ ONBOARDING — Phase 3 of 3: First Post ║ +╚═══════════════════════════════════════════╝ +``` + +Check `first_post_date` in state file: + +**If null (no first post yet):** +- "You're ready to create your first post! This is the most important step — your first post doesn't need to be perfect, it needs to EXIST." +- Use AskUserQuestion: + 1. **Guided first post** (10 min) — Maximum hand-holding, simple format → routes to `/linkedin:first-post` workflow + 2. **Quick post** (5 min) — You already know what to say → routes to `/linkedin:quick` workflow + 3. **Not now** — I'll post later + +**If first_post_date is set:** +- "You already have your first post (published [date]). Ready to create your next one?" +- Use AskUserQuestion: + 1. **Create a new post** → suggest `/linkedin:post` + 2. **Quick post** → suggest `/linkedin:quick` + 3. **Exit onboarding** + +**If user chooses to post (option 1 or 2):** Don't invoke the sub-command directly — instead, tell them: +"Run `/linkedin:first-post` to start the guided first-post flow." +or +"Run `/linkedin:quick` to create a quick post." + +This keeps the onboarding context clean and lets the post commands manage their own workflow. + +## Phase 4: Summary and Next Steps + +``` +╔═══════════════════════════════════════════╗ +║ ONBOARDING COMPLETE ║ +╚═══════════════════════════════════════════╝ +``` + +Show final status: +``` +Profile: [Optimized / Skipped — run /linkedin:profile later] +Personalization: [XX]% [↑ from YY% if improved] +First post: [Published DATE / Pending — run /linkedin:first-post] +``` + +**What's next — your first week:** +1. Create 2-3 posts this week (`/linkedin:post` or `/linkedin:quick`) +2. Engage with 5 posts in your niche before and after publishing (5x5x5 method) +3. Import your first analytics data after 7 days (`/linkedin:import`) +4. Run `/linkedin:report` after your first week to see what's working + +**Power commands to explore:** +- `/linkedin:batch` — Plan a full week of content in one session +- `/linkedin:react` — Turn articles and news into posts +- `/linkedin:strategy` — Growth strategy tailored to your follower level +- `/linkedin` — See all 25 commands anytime diff --git a/plugins/linkedin-thought-leadership/commands/post.md b/plugins/linkedin-thought-leadership/commands/post.md index 52c12f3..5a232c5 100644 --- a/plugins/linkedin-thought-leadership/commands/post.md +++ b/plugins/linkedin-thought-leadership/commands/post.md @@ -23,16 +23,6 @@ You are a LinkedIn thought leadership content creator. Guide the user through cr First, load persistent state and personalization: - Read `~/.claude/linkedin-thought-leadership.local.md` for posting state (streak, weekly progress, recent topics) - Read `skills/linkedin-thought-leadership/SKILL.md` for user profile, voice settings, and preferences -- Read `config/user-profile.local.md` (if it exists) for expertise areas and audience - -### Readiness Check - -If `config/user-profile.local.md` doesn't exist OR `assets/voice-samples/authentic-voice-samples.md` contains `[Your Name]` in the title line, show this non-blocking notice: - -> This plugin isn't personalized yet. Content will use generic best practices. -> Run `/linkedin:setup` after this session to unlock voice-matched content. - -Then proceed normally — do not block content creation. Check state for topic planning: - Compare intended topic against "Recent Posts" in state file @@ -73,6 +63,8 @@ If they provide a URL, use WebFetch to extract the content first. Read `references/thought-leadership-angles.md` for the 8 universal angles. +**Industry-specific angles:** If `config/user-profile.local.md` exists and has an `industry` field, check the "Industry Angle Variants" section in `thought-leadership-angles.md` for the matching industry table. Use the industry-specific starter questions and example hooks to generate more targeted angle suggestions. + Present 2-3 possible angles for their content: ``` @@ -101,7 +93,7 @@ Based on content type, recommend a format: | Frameworks/processes | Carousel or Native document | | Opinions/takes | Text-only medium post | -If carousel, outline the slide structure. +If carousel is the best format, recommend: "This topic works great as a carousel. Run `/linkedin:carousel` for the full slide-by-slide generator with 5 proven templates." ## Step 4: Structure and Write diff --git a/plugins/linkedin-thought-leadership/commands/quick.md b/plugins/linkedin-thought-leadership/commands/quick.md index 3da472e..897d5b8 100644 --- a/plugins/linkedin-thought-leadership/commands/quick.md +++ b/plugins/linkedin-thought-leadership/commands/quick.md @@ -27,17 +27,6 @@ Read `skills/linkedin-thought-leadership/SKILL.md` for: - Core expertise areas (for topical alignment) - Phrases they commonly use -Read `config/user-profile.local.md` (if it exists) for expertise areas and audience. - -### Readiness Check - -If `config/user-profile.local.md` doesn't exist OR `assets/voice-samples/authentic-voice-samples.md` contains `[Your Name]` in the title line, show this non-blocking notice: - -> This plugin isn't personalized yet. Content will use generic best practices. -> Run `/linkedin:setup` after this session to unlock voice-matched content. - -Then proceed normally — do not block content creation. - Read `assets/quick-post-resources.md` for: - Hooks bank - CTAs bank diff --git a/plugins/linkedin-thought-leadership/commands/react.md b/plugins/linkedin-thought-leadership/commands/react.md index 8568918..b2ae364 100644 --- a/plugins/linkedin-thought-leadership/commands/react.md +++ b/plugins/linkedin-thought-leadership/commands/react.md @@ -26,7 +26,7 @@ First, load persistent state and personalization: - Read `assets/voice-samples/authentic-voice-samples.md` for voice profile - Check recent posts to avoid topic repetition within 7 days -## Step 1: Get the URL +## Step 1: Get URL(s) If the user hasn't provided a URL, ask for one. Accept: - News articles @@ -36,6 +36,17 @@ If the user hasn't provided a URL, ask for one. Accept: - Company announcements - Social media threads +**Multiple URLs:** If the user provides 2-3 URLs, or if you detect multiple links, use AskUserQuestion: + +``` +I see multiple URLs. Would you like to: +1. React to a single article (pick the most interesting one) +2. Compare and contrast 2-3 articles into one post +``` + +If option 2 → jump to **Comparison Path** (Step 1b below). +If option 1 or single URL → continue to Step 2. + ## Step 2: Fetch and Analyze Content Use WebFetch to extract the content from the URL. Ask WebFetch to extract: @@ -143,6 +154,96 @@ After the post is finalized, update `~/.claude/linkedin-thought-leadership.local - Update `longest_streak` if current exceeds it - Add entry to "## Recent Posts": [YYYY-MM-DD] "Hook text..." (char count) - topic +--- + +## Comparison Path (Multi-URL) + +When the user wants to compare 2-3 articles into one post. + +### Step 1b: Collect URLs + +Collect 2-3 URLs. Minimum 2, maximum 3. If the user provided them already, confirm the list. + +### Step 2b: Fetch All Sources + +Use WebFetch on each URL. For each, extract: +- **Title** and author/source +- **Key claims** (3-5 bullet points) +- **Stance/argument** — what position does the author take? +- **Data points** — any statistics or evidence cited + +### Step 3b: Synthesis Analysis + +Analyze across all sources: + +| Dimension | Analysis | +|-----------|----------| +| **Common ground** | Where do the sources agree? | +| **Tension points** | Where do they disagree or contradict? | +| **Blind spots** | What are ALL of them missing? | +| **Your unique angle** | Given your expertise, what perspective do you add? | + +### Step 4b: Choose Comparison Angle + +Present 3 angles via AskUserQuestion: + +1. **Synthesis** — "These perspectives seem opposed, but the truth is more nuanced. Here's how I connect them." +2. **Contrarian to all** — "Both/all articles miss the real issue. Here's what actually matters." +3. **Pattern analysis** — "The fact that [N] experts are all writing about [X] tells us something about [Y]." + +### Step 5b: Generate Comparison Draft + +Structure: + +**Hook (110-140 chars):** Your synthesized perspective — NOT "I read 3 articles about..." Avoid mentioning the number of sources in the hook. + +**The conversation (1-2 sentences):** Briefly describe the debate or trend ("There's a growing conversation about [X]. Perspectives range from [A] to [B].") + +**Your lens (main body):** +- What the synthesis reveals that individual pieces miss +- Concrete example from your experience that connects the dots +- Where you agree and where you push back + +**Implication (1-2 sentences):** What this convergence/divergence means for the audience. + +**CTA:** Question that invites people to take a side or share their own synthesis. + +### Critical Rules (comparison-specific): +- **NO URLs in post body** — all links go in first comment +- Post must stand alone without reading any of the sources +- Don't summarize each article — synthesize across them +- Your perspective is the star, not the articles +- Character target: 1,200-1,800 chars + +### Step 6b: Quality Check + +Same as Step 6, plus: +- [ ] Post is a synthesis, not a summary of each article +- [ ] Hook doesn't mention number of sources read +- [ ] Each source is credited in the first comment, not the post + +### Step 7b: Present Draft + +Show: +1. The main draft with character count +2. 2 alternative hooks +3. Suggested first comment with ALL URLs: + ``` + Sources referenced: + 1. "[Title]" by [Author] — [URL] + 2. "[Title]" by [Author] — [URL] + 3. "[Title]" by [Author] — [URL] (if applicable) + ``` +4. Recommended posting time + +Offer same refinement options as Step 7. + +### Step 8b: State Update + +Same as Step 8 — update state file with topic, increment counts, etc. + +--- + ## Reference Files - `assets/voice-samples/authentic-voice-samples.md` — Voice matching diff --git a/plugins/linkedin-thought-leadership/commands/report.md b/plugins/linkedin-thought-leadership/commands/report.md index 137c497..96c971f 100644 --- a/plugins/linkedin-thought-leadership/commands/report.md +++ b/plugins/linkedin-thought-leadership/commands/report.md @@ -37,18 +37,32 @@ You need to import your LinkedIn analytics first: 1. Run `/linkedin:import` to import CSV data 2. Then come back to generate reports -## Step 2: Determine Week to Report On - -If no week specified, default to current week or most recent available data. +## Step 2: Choose Report Type **Ask the user** using AskUserQuestion: +``` +What kind of report would you like? + +1. Weekly report (default) — performance for a specific ISO week +2. Monthly report — month summary with month-over-month comparison +3. Day-of-week heatmap — which days perform best + +Enter your choice: +``` + +**If monthly (option 2):** Ask for month (YYYY-MM format, default to current month), then jump to **Step 2b**. +**If heatmap (option 3):** Run the heatmap CLI command and jump to **Step 6c**. +**If weekly (option 1 or default):** Continue below. + +### Weekly: Determine Week + ``` Which week would you like a report for? Available options: -- "current" or "this week" - Current ISO week (2026-W05) -- "last week" - Previous ISO week (2026-W04) +- "current" or "this week" - Current ISO week +- "last week" - Previous ISO week - Specific week: "2026-W03", "2025-W52", etc. - "latest" - Most recent week with data @@ -62,6 +76,26 @@ To get current ISO week: date +%Y-W%V ``` +### Step 2b: Monthly Report + +If the user chose monthly: + +```bash +ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics" node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --month +``` + +Read the generated JSON from `assets/analytics/monthly-reports/.json`. Present the monthly summary with MoM comparison deltas, weekly breakdown, and top performers. Then jump to Step 7 for deep-dive options. + +### Step 2c: Heatmap + +If the user chose heatmap: + +```bash +ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics" node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" heatmap +``` + +Present the day-of-week matrix and best-day findings. Then jump to Step 7 for deep-dive options. + ## Step 3: Run Report Generation Execute the report CLI command: diff --git a/plugins/linkedin-thought-leadership/hooks/prompts/post-creation-automation.md b/plugins/linkedin-thought-leadership/hooks/prompts/post-creation-automation.md index 5137bce..d736c03 100644 --- a/plugins/linkedin-thought-leadership/hooks/prompts/post-creation-automation.md +++ b/plugins/linkedin-thought-leadership/hooks/prompts/post-creation-automation.md @@ -16,14 +16,7 @@ Based on the day of the week, suggest the next optimal posting window: - Weekend: 10-11 AM CET (lower reach but less competition) **3. 5x5x5 Engagement Reminder** -Remind the user about the 5x5x5 engagement ritual: - -> **5x5x5 Engagement Ritual** (15-20 min before AND in the first hour after posting): -> - **5 comments** — find 5 people with overlapping audiences and leave thoughtful comments on their recent posts -> - **5 connection requests** — send personalized requests to people who engaged with your niche today -> - **5 replies** — reply to every comment on YOUR post within the first hour -> -> This signals active participation to LinkedIn's algorithm and boosts your post's initial distribution. +Remind: 'Before posting, spend 15-20 minutes on 5x5x5 pre-engagement: find 5 people with overlapping audiences, comment thoughtfully on their recent posts.' **4. Content Logging** Note: The post topic and hook should be logged to the state file when the session ends (handled by Stop hook). diff --git a/plugins/linkedin-thought-leadership/hooks/prompts/state-update-reminder.md b/plugins/linkedin-thought-leadership/hooks/prompts/state-update-reminder.md index 76bdcc2..1f4e70e 100644 --- a/plugins/linkedin-thought-leadership/hooks/prompts/state-update-reminder.md +++ b/plugins/linkedin-thought-leadership/hooks/prompts/state-update-reminder.md @@ -38,19 +38,20 @@ If a scheduled post was published during this session: Provide reminders naturally based on what was done in the session. If no LinkedIn content was created, skip the reminders and just ensure state is consistent. -**4. Voice Sample Extraction** (if a post was created) +**4. Voice Sample Collection** (if a post was created) -If a LinkedIn post was created or finalized in this session, consider extracting the hook line as a voice sample: +If a LinkedIn post was created or finalized in this session, save the full post text as a voice sample: -- Read the hook line from the post that was just created +- Read the full post text from the draft that was just created - Check if `assets/voice-samples/authentic-voice-samples.md` exists -- If it does, suggest appending a new entry to the "## Update Log" section at the bottom: +- Append the full post to the `## Collected Post Samples` section: ``` - - [YYYY-MM-DD]: "[Hook text]" — [post type] (extracted from session post) + ### [YYYY-MM-DD] — [post type] ([char count] chars) + [Full post text exactly as written] ``` -- **Ask the user for approval before writing.** Say: "Would you like me to save this hook as a voice sample for future reference?" -- Only write if the user approves -- This passively grows the voice profile over time, improving personalization score +- **Ask the user for confirmation** before writing: "I'll save this post as a voice sample for drift detection. OK?" +- This builds the voice sample library that enables automatic drift scoring (needs 5+ samples for reliable scoring) +- The more samples collected, the more accurate the voice-trainer's drift detection becomes **5. Content History Log** (if a post was created) diff --git a/plugins/linkedin-thought-leadership/hooks/prompts/voice-guardian.md b/plugins/linkedin-thought-leadership/hooks/prompts/voice-guardian.md index d283200..caa8ee0 100644 --- a/plugins/linkedin-thought-leadership/hooks/prompts/voice-guardian.md +++ b/plugins/linkedin-thought-leadership/hooks/prompts/voice-guardian.md @@ -1,6 +1,7 @@ -VOICE GUARDIAN — AI AUTHENTICITY CHECK: If the file being written/edited is LinkedIn content (post draft, article, or content file — NOT config, state, scripts, docs), check for AI-sounding patterns: +VOICE GUARDIAN — DRIFT SCORING & AI AUTHENTICITY CHECK: If the file being written/edited is LinkedIn content (post draft, article, or content file — NOT config, state, scripts, docs), perform both AI detection and voice drift scoring: + +## 1. AI Pattern Detection -**AI Pattern Detection:** Scan for these common AI writing patterns: - Generic openings: 'In today's rapidly evolving...', 'As we navigate...', 'In the ever-changing landscape...' - Filler phrases: 'It's worth noting that', 'It goes without saying', 'At the end of the day' @@ -10,13 +11,42 @@ Scan for these common AI writing patterns: - Hedging language: 'It could be argued', 'One might say', 'Perhaps' - Perfect structure: Every paragraph exactly the same length -**Authenticity Score:** If 3+ AI patterns detected, flag: 'Voice Guardian Alert: This content scores below authenticity threshold. AI patterns found: [list specific patterns]. Suggested fixes: [specific rewrites using natural language].' -**Voice Matching:** -If voice samples exist at `${CLAUDE_PLUGIN_ROOT}/assets/voice-samples/`, compare the writing style against the user's authentic voice patterns. Flag deviations. +## 2. Six-Dimension Voice Drift Scoring + +Read the voice profile and collected post samples from `${CLAUDE_PLUGIN_ROOT}/assets/voice-samples/authentic-voice-samples.md`. + +Score the draft against these 6 dimensions (0 = perfect match, 1 = minor drift per dimension): + +| Dimension | What to Compare | +|-----------|----------------| +| **Sentence structure** | Average length, complexity, use of fragments vs. compound sentences | +| **Word choice** | Vocabulary level, preferred/avoided words from voice profile | +| **Opening patterns** | Hook style — does it match the user's signature openers? | +| **Storytelling** | Anecdote usage, narrative arc, concrete vs. abstract | +| **Tone markers** | Humor, directness, formality level, empathy signals | +| **Formatting** | Paragraph length, whitespace, emoji usage, punctuation habits | + +**Sum the 6 scores (0-6 total) and output a verdict:** + +| Score | Verdict | Action | +|-------|---------|--------| +| 0-1 | AUTHENTIC | No changes needed | +| 2-3 | CAUTION | Flag specific dimensions that drifted, suggest fixes | +| 4-5 | ALERT | Significant drift — list all deviating dimensions with rewrites | +| 6 | REWRITE | Content doesn't sound like the user — recommend starting over | + +**Confidence gate:** If `## Collected Post Samples` has fewer than 5 posts, output: "Voice drift: LOW CONFIDENCE (X/5 samples). Scoring based on voice profile only." and score only against the profile description (dimensions 1-2 and 4-6), skipping opening patterns (dimension 3) which needs real samples. + +**Output format (always include at end of system message):** +``` +Voice Drift: [VERDICT] ([score]/6) [confidence: HIGH/LOW] +[If CAUTION+: list dimensions that scored 1 with brief fix suggestion] +``` + +## 3. Humanization Tips (for CAUTION or higher) -**Humanization Tips:** - Add specific personal anecdotes or observations - Use conversational contractions (I've, don't, it's) - Include imperfect/real-world examples diff --git a/plugins/linkedin-thought-leadership/hooks/scripts/__tests__/week-rollover.test.mjs b/plugins/linkedin-thought-leadership/hooks/scripts/__tests__/week-rollover.test.mjs new file mode 100644 index 0000000..379d843 --- /dev/null +++ b/plugins/linkedin-thought-leadership/hooks/scripts/__tests__/week-rollover.test.mjs @@ -0,0 +1,102 @@ +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { applyWeekRollover } from '../week-rollover.mjs'; + +const SAMPLE_STATE = `--- +last_post_date: "2026-04-05" +first_post_date: "2026-01-15" +last_post_topic: "AI strategy" +posts_this_week: 3 +weekly_goal: 3 +current_streak: 5 +longest_streak: 12 +current_week: "2026-W14" +last_import_date: "2026-04-01" +follower_count: 850 +follower_target: 10000 +target_date: "2026-12-31" +--- + +## Recent Posts +- 2026-04-05: AI strategy post +`; + +describe('applyWeekRollover', () => { + test('resets posts_this_week to 0 on week change', () => { + const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W15'); + assert.notEqual(result, null); + assert.match(result.content, /^posts_this_week: 0$/m); + }); + + test('updates current_week to new week', () => { + const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W15'); + assert.notEqual(result, null); + assert.match(result.content, /^current_week: "2026-W15"$/m); + }); + + test('returns descriptive message on rollover', () => { + const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W15'); + assert.notEqual(result, null); + assert.ok(result.message.includes('2026-W15')); + assert.ok(result.message.includes('2026-W14')); + }); + + test('returns null when week matches (no change needed)', () => { + const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W14'); + assert.equal(result, null); + }); + + test('preserves all other YAML fields unchanged', () => { + const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W15'); + assert.notEqual(result, null); + assert.match(result.content, /^last_post_date: "2026-04-05"$/m); + assert.match(result.content, /^current_streak: 5$/m); + assert.match(result.content, /^weekly_goal: 3$/m); + assert.match(result.content, /^follower_count: 850$/m); + }); + + test('preserves markdown body after frontmatter', () => { + const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W15'); + assert.notEqual(result, null); + assert.ok(result.content.includes('## Recent Posts')); + assert.ok(result.content.includes('AI strategy post')); + }); + + test('initializes current_week when empty without resetting posts', () => { + const stateWithEmptyWeek = SAMPLE_STATE.replace( + 'current_week: "2026-W14"', + 'current_week: ""' + ); + const result = applyWeekRollover(stateWithEmptyWeek, '', '2026-W15'); + assert.notEqual(result, null); + assert.match(result.content, /^current_week: "2026-W15"$/m); + // posts_this_week should NOT be reset (user may have manually tracked) + assert.match(result.content, /^posts_this_week: 3$/m); + }); + + test('returns null when actualWeek is empty', () => { + const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', ''); + assert.equal(result, null); + }); + + test('returns null when actualWeek is null/undefined', () => { + assert.equal(applyWeekRollover(SAMPLE_STATE, '2026-W14', null), null); + assert.equal(applyWeekRollover(SAMPLE_STATE, '2026-W14', undefined), null); + }); + + test('handles year boundary rollover (W52 → W01)', () => { + const yearEndState = SAMPLE_STATE.replace('2026-W14', '2025-W52'); + const result = applyWeekRollover(yearEndState, '2025-W52', '2026-W01'); + assert.notEqual(result, null); + assert.match(result.content, /^posts_this_week: 0$/m); + assert.match(result.content, /^current_week: "2026-W01"$/m); + }); + + test('handles posts_this_week already at 0', () => { + const zeroPostsState = SAMPLE_STATE.replace('posts_this_week: 3', 'posts_this_week: 0'); + const result = applyWeekRollover(zeroPostsState, '2026-W14', '2026-W15'); + assert.notEqual(result, null); + assert.match(result.content, /^posts_this_week: 0$/m); + assert.match(result.content, /^current_week: "2026-W15"$/m); + }); +}); diff --git a/plugins/linkedin-thought-leadership/hooks/scripts/session-start.mjs b/plugins/linkedin-thought-leadership/hooks/scripts/session-start.mjs index bd61878..9ee2816 100644 --- a/plugins/linkedin-thought-leadership/hooks/scripts/session-start.mjs +++ b/plugins/linkedin-thought-leadership/hooks/scripts/session-start.mjs @@ -7,6 +7,7 @@ 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, '..', '..'); @@ -135,11 +136,17 @@ if (existsSync(STATE_FILE)) { } } - // Week rollover check + // Week rollover — auto-reset posts_this_week on week change 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.`; + 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.`; } // Build status line @@ -253,9 +260,7 @@ if (existsSync(STATE_FILE)) { } // Personalization score check - if (pScore !== null && pScore === 0) { - context += '## Quick Win\\nPersonalization: 0%. Run /linkedin:setup (15 min) to unlock voice-matched, audience-specific content.\\n\\n'; - } else if (pScore !== null && pScore < 50) { + 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`; } @@ -369,13 +374,8 @@ if (existsSync(STATE_FILE)) { 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\\n`; - context += '## Welcome to LinkedIn Thought Leadership\\n\\n'; - context += 'Your state file has been initialized. Here is how to get started:\\n\\n'; - context += '1. Run /linkedin:profile — Optimize your LinkedIn profile for 360Brew (critical before first post)\\n'; - context += '2. Run /linkedin:setup — Personalize with your voice, case studies, and audience data\\n'; - context += '3. Run /linkedin:first-post — Create your first post in under 10 minutes\\n\\n'; - context += 'Your personalization score is 0%. Content quality improves as you fill in your profile.\\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`; diff --git a/plugins/linkedin-thought-leadership/hooks/scripts/week-rollover.mjs b/plugins/linkedin-thought-leadership/hooks/scripts/week-rollover.mjs new file mode 100644 index 0000000..d3e4a47 --- /dev/null +++ b/plugins/linkedin-thought-leadership/hooks/scripts/week-rollover.mjs @@ -0,0 +1,49 @@ +// Pure function for week-rollover logic. +// Exported separately for testability. + +/** + * Apply week rollover to state file content. + * Returns updated content string if rollover was applied, null otherwise. + * + * @param {string} stateContent - Full state file content (with YAML frontmatter) + * @param {string} currentWeek - Week value from state file (e.g. "2026-W14") + * @param {string} actualWeek - Computed current ISO week (e.g. "2026-W15") + * @returns {{ content: string, message: string } | null} + */ +export function applyWeekRollover(stateContent, currentWeek, actualWeek) { + if (!actualWeek) return null; + + // Case 1: current_week is empty — initialize without resetting posts + if (!currentWeek) { + const updated = stateContent.replace( + /^current_week: .*/m, + `current_week: "${actualWeek}"` + ); + if (updated === stateContent) return null; + return { + content: updated, + message: `Initialized current_week to ${actualWeek}.` + }; + } + + // Case 2: week matches — no action needed + if (currentWeek === actualWeek) return null; + + // Case 3: week changed — reset posts_this_week and update current_week + let updated = stateContent; + updated = updated.replace( + /^posts_this_week: .*/m, + 'posts_this_week: 0' + ); + updated = updated.replace( + /^current_week: .*/m, + `current_week: "${actualWeek}"` + ); + + if (updated === stateContent) return null; + + return { + content: updated, + message: `Auto-reset: posts_this_week → 0 for new week ${actualWeek} (was ${currentWeek}).` + }; +} diff --git a/plugins/linkedin-thought-leadership/references/thought-leadership-angles.md b/plugins/linkedin-thought-leadership/references/thought-leadership-angles.md index 67daf81..ac238fd 100644 --- a/plugins/linkedin-thought-leadership/references/thought-leadership-angles.md +++ b/plugins/linkedin-thought-leadership/references/thought-leadership-angles.md @@ -122,6 +122,88 @@ These angles work across all industries because they're about **types of thinkin - **Education:** Personal Lesson + Human Story - **Consulting:** Pattern Recognition + Practical Breakdown +## Industry Angle Variants + +Concrete starter questions and example hooks per industry. When the user's industry is known (from `config/user-profile.local.md`), surface the relevant table during angle selection. + +### Tech / Software / AI + +| Angle | Starter Question | Example Hook | +|-------|-----------------|--------------| +| Contrarian | "What does everyone assume about [tech trend] that data disproves?" | "Everyone says AI will replace developers. Our team shipped 40% more code WITH AI — and hired 3 more engineers." | +| Pattern Recognition | "What pattern across AI/cloud/DevOps haven't others connected?" | "I've noticed every team that fails at AI adoption makes the same infrastructure mistake first." | +| Uncomfortable Truth | "What is the industry avoiding saying about [tool/trend]?" | "We spent 6 months fine-tuning an LLM. A prompt template outperformed it in 2 hours." | +| Future Implication | "If [current trend] continues, what changes in 2-3 years?" | "If AI coding assistants keep improving at this rate, the most valuable developer skill in 2028 won't be coding." | +| Personal Lesson | "What did your last failed project teach you about [topic]?" | "Our AI pilot looked perfect in the demo. Here's what happened when real users touched it." | +| Reframe | "What common tech term means something different than people think?" | "We call it 'technical debt.' I call it 'decisions that were right then and wrong now.'" | +| Practical Breakdown | "What complex concept can you make actionable in 5 steps?" | "Everyone talks about RAG. Here's the 4-step checklist I use before building any retrieval system." | +| Human Story | "What moment with a colleague or user changed your perspective?" | "Our senior architect said 'I don't understand this AI stuff' in a meeting. What happened next changed our entire approach." | + +### Healthcare / Life Sciences + +| Angle | Starter Question | Example Hook | +|-------|-----------------|--------------| +| Contrarian | "What healthcare 'best practice' actually slows patient outcomes?" | "We digitized all our patient records. Patient satisfaction dropped. Here's why paper had one advantage we overlooked." | +| Pattern Recognition | "What pattern connects clinical and operational challenges?" | "I've worked with 12 hospitals this year. The ones with the best patient outcomes all share one non-clinical habit." | +| Uncomfortable Truth | "What is healthcare leadership not willing to discuss openly?" | "The biggest barrier to healthcare AI isn't regulation. It's that clinicians don't trust their own data." | +| Future Implication | "If [health tech trend] succeeds, what changes for patients?" | "If ambient clinical documentation works as promised, the doctor-patient relationship fundamentally changes." | +| Personal Lesson | "What did a patient interaction teach you about [system/process]?" | "A patient told me: 'Your portal has 47 clicks to book an appointment.' That sentence restructured our entire digital strategy." | +| Reframe | "What healthcare metric measures the wrong thing?" | "We measure 'patient throughput.' What if we measured 'patient understanding' instead?" | +| Practical Breakdown | "What regulatory/compliance challenge can you simplify?" | "HIPAA compliance for AI tools sounds impossible. Here are the 3 questions that solve 80% of the uncertainty." | +| Human Story | "What patient story illustrates a systemic issue?" | "A nurse spent 4 hours on documentation for every 1 hour of patient care. She quit. Her exit interview should be mandatory reading for every CIO." | + +### Finance / Banking / Insurance + +| Angle | Starter Question | Example Hook | +|-------|-----------------|--------------| +| Contrarian | "What financial 'innovation' is actually recycled risk?" | "Everyone's excited about embedded finance. The banks that remember 2008 are asking different questions." | +| Pattern Recognition | "What pattern connects fintech disruption and traditional banking?" | "I've noticed every fintech that struggles at scale hits the same wall — the one banks solved 30 years ago." | +| Uncomfortable Truth | "What is the industry avoiding about [regulation/risk/AI]?" | "Banks are spending millions on AI fraud detection. The fraud teams say the biggest vulnerability is still a phone call." | +| Future Implication | "If [regulatory change] passes, what does banking look like?" | "If open banking delivers on its promise, the most valuable asset in finance won't be capital — it'll be consent." | +| Personal Lesson | "What did a risk event teach you that no framework captures?" | "We built a perfect risk model. It missed the one variable that mattered: human panic." | +| Reframe | "What financial concept needs a new definition?" | "We call it 'customer acquisition cost.' But in financial services, the real cost is trust — and trust doesn't have a line item." | +| Practical Breakdown | "What compliance requirement can you make less painful?" | "RegTech sounds complex. Here's the 3-layer approach that cut our compliance reporting time by 60%." | +| Human Story | "What client interaction revealed a blind spot?" | "A small business owner asked me: 'Why does your app need to know my mother's maiden name to send an invoice?' Fair point." | + +### Public Sector / Government + +| Angle | Starter Question | Example Hook | +|-------|-----------------|--------------| +| Contrarian | "What public sector 'modernization' approach actually creates more bureaucracy?" | "We 'digitized' our forms by turning PDFs into web forms. Citizens still needed to visit the office. That's not digital transformation." | +| Pattern Recognition | "What pattern connects successful government IT projects?" | "I've studied 20 public sector IT projects. The 5 that succeeded all broke the same procurement rule." | +| Uncomfortable Truth | "What is the sector avoiding about [digital transformation/AI/procurement]?" | "The biggest obstacle to government AI isn't budget or policy. It's that we measure success by project completion, not citizen outcome." | +| Future Implication | "If [policy/tech] is adopted, what changes for citizens?" | "If government agencies actually share data across departments, we can stop asking citizens to prove who they are 47 times." | +| Personal Lesson | "What did a failed initiative teach you about public sector change?" | "We launched a citizen portal. 6 months later, the call center was busier than ever. The lesson wasn't about technology." | +| Reframe | "What government process looks different from the citizen's perspective?" | "We call it 'case processing.' Citizens call it 'waiting to hear if I can keep my home.'" | +| Practical Breakdown | "What complex regulation/process can you make tangible?" | "Government procurement for AI services sounds impossible. Here are 3 contract clauses that unlock 80% of the innovation." | +| Human Story | "What citizen interaction changed how you think about service delivery?" | "A retired teacher spent 3 hours navigating our website for a pension form. She said: 'I taught 2,000 students to learn. Your website taught me to give up.'" | + +### Education / EdTech + +| Angle | Starter Question | Example Hook | +|-------|-----------------|--------------| +| Contrarian | "What education 'innovation' actually hurts learning outcomes?" | "We gave every student a laptop. Test scores didn't change. Classroom engagement dropped. Here's what we missed." | +| Pattern Recognition | "What do successful learning programs have in common?" | "I've observed 15 AI-in-education pilots. The ones students actually use all share one design principle." | +| Uncomfortable Truth | "What is the sector avoiding about [AI/assessment/equity]?" | "Personalized learning algorithms optimize for engagement. But engagement and learning aren't the same thing." | +| Future Implication | "If [AI/policy trend] continues, how does education change?" | "If AI tutors become genuinely good, the teacher's most valuable skill won't be content delivery — it'll be asking the right question at the right moment." | +| Personal Lesson | "What did a student/classroom experience teach you?" | "I watched a student use ChatGPT to write an essay, then spent 2 hours explaining it to a classmate. That's when I realized the assignment was wrong, not the student." | +| Reframe | "What education metric measures the wrong thing?" | "We measure 'time on task.' What if the best indicator of learning is how quickly a student can teach it to someone else?" | +| Practical Breakdown | "What complex pedagogical concept can you make actionable?" | "Bloom's Taxonomy is in every education textbook. Here's how I actually use it to design a single lesson in 15 minutes." | +| Human Story | "What student moment illustrates a bigger truth?" | "A 10-year-old told me: 'Why do I have to learn this if I can just ask AI?' My answer surprised both of us." | + +### Consulting / Professional Services + +| Angle | Starter Question | Example Hook | +|-------|-----------------|--------------| +| Contrarian | "What consulting 'framework' actually prevents insight?" | "The best strategy I ever delivered had zero frameworks. It had one question the CEO couldn't answer." | +| Pattern Recognition | "What pattern connects client problems across industries?" | "I've worked with 30 organizations on AI strategy. The ones that succeed all start with the same non-technical conversation." | +| Uncomfortable Truth | "What is the industry avoiding about [value delivery/pricing/AI]?" | "Most consulting engagements solve the stated problem. The real problem — the one nobody mentioned in the RFP — stays unsolved." | +| Future Implication | "If [AI/market trend] continues, how does consulting change?" | "If AI can generate a strategy deck in 10 minutes, the consulting industry needs to answer one question: what are we actually selling?" | +| Personal Lesson | "What project failure taught you something the methodology didn't?" | "I delivered a perfect change management plan. The client implemented 10% of it. My methodology was right. My assumption about people was wrong." | +| Reframe | "What consulting term means something different than clients think?" | "Clients ask for 'digital transformation.' What they actually need is 'permission to stop doing things that don't work.'" | +| Practical Breakdown | "What complex client challenge can you simplify?" | "AI readiness assessments take 6 weeks and cost €200K. Here are the 5 questions that tell you 80% of what you need in one meeting." | +| Human Story | "What client moment changed your consulting approach?" | "A CTO told me: 'Your recommendation is brilliant. My team will ignore it by Thursday.' That conversation changed how I deliver every project." | + ## Red Flags (Avoid These) - **Echo chamber:** Repeating what everyone already says diff --git a/plugins/linkedin-thought-leadership/scripts/analytics/src/cli.ts b/plugins/linkedin-thought-leadership/scripts/analytics/src/cli.ts index 2dbc512..deb8fda 100644 --- a/plugins/linkedin-thought-leadership/scripts/analytics/src/cli.ts +++ b/plugins/linkedin-thought-leadership/scripts/analytics/src/cli.ts @@ -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 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); diff --git a/plugins/linkedin-thought-leadership/scripts/analytics/src/models/types.ts b/plugins/linkedin-thought-leadership/scripts/analytics/src/models/types.ts index b8a30be..77b323f 100644 --- a/plugins/linkedin-thought-leadership/scripts/analytics/src/models/types.ts +++ b/plugins/linkedin-thought-leadership/scripts/analytics/src/models/types.ts @@ -65,6 +65,55 @@ export interface Alert { deviations: number; } +export interface DayOfWeekMetrics { + dayName: string; // "Monday" through "Sunday" + dayIndex: number; // 1=Monday, 7=Sunday (ISO weekday) + postCount: number; + avgImpressions: number; + avgEngagementRate: number; + bestPost?: PostAnalytics; +} + +export interface HeatmapReport { + generatedAt: string; + postsAnalyzed: number; + dateRange: { from: string; to: string }; + byDayOfWeek: DayOfWeekMetrics[]; // 7 entries, Mon-Sun ordered + bestDayImpressions: string; + bestDayEngagement: string; +} + +export interface MonthlyReport { + month: string; // "YYYY-MM" + generatedAt: string; + summary: { + totalPosts: number; + totalImpressions: number; + totalReactions: number; + totalComments: number; + totalShares: number; + totalClicks: number; + avgEngagementRate: number; + avgImpressionsPerPost: number; + }; + topPerformers: PostAnalytics[]; + byWeek: { + week: string; + postCount: number; + avgImpressions: number; + avgEngagementRate: number; + }[]; + trends: { + comparedTo: string | null; + percentChange: { + impressions: number | null; + engagement: number | null; + postCount: number | null; + }; + }; + alerts: Alert[]; +} + export const ALERT_THRESHOLDS = { spike: 2.0, drop: -1.5, diff --git a/plugins/linkedin-thought-leadership/scripts/analytics/src/reports/heatmap.ts b/plugins/linkedin-thought-leadership/scripts/analytics/src/reports/heatmap.ts new file mode 100644 index 0000000..591592f --- /dev/null +++ b/plugins/linkedin-thought-leadership/scripts/analytics/src/reports/heatmap.ts @@ -0,0 +1,85 @@ +import type { PostAnalytics, DayOfWeekMetrics, HeatmapReport } from "../models/types.js"; + +const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + +// Convert JS getDay() (0=Sun) to ISO weekday (1=Mon, 7=Sun) +function toISOWeekday(jsDay: number): number { + return jsDay === 0 ? 7 : jsDay; +} + +/** + * Generate a day-of-week performance heatmap from post analytics data. + * Groups posts by day of week and calculates average metrics per day. + */ +export function generateHeatmap(posts: PostAnalytics[]): HeatmapReport { + // Initialize buckets for all 7 days (ISO: 1=Mon to 7=Sun) + const buckets: Map = new Map(); + for (let i = 1; i <= 7; i++) { + buckets.set(i, []); + } + + // Group posts by ISO weekday + for (const post of posts) { + const jsDay = new Date(post.publishedDate).getUTCDay(); + const isoDay = toISOWeekday(jsDay); + buckets.get(isoDay)!.push(post); + } + + // Build metrics per day + const byDayOfWeek: DayOfWeekMetrics[] = []; + for (let isoDay = 1; isoDay <= 7; isoDay++) { + const dayPosts = buckets.get(isoDay)!; + const jsDay = isoDay === 7 ? 0 : isoDay; + const dayName = DAY_NAMES[jsDay]; + + if (dayPosts.length === 0) { + byDayOfWeek.push({ + dayName, + dayIndex: isoDay, + postCount: 0, + avgImpressions: 0, + avgEngagementRate: 0, + }); + continue; + } + + const totalImpressions = dayPosts.reduce((sum, p) => sum + p.metrics.impressions, 0); + const totalEngagement = dayPosts.reduce((sum, p) => sum + p.metrics.engagementRate, 0); + const bestPost = dayPosts.reduce((best, p) => + p.metrics.impressions > best.metrics.impressions ? p : best + ); + + byDayOfWeek.push({ + dayName, + dayIndex: isoDay, + postCount: dayPosts.length, + avgImpressions: Math.round(totalImpressions / dayPosts.length), + avgEngagementRate: parseFloat((totalEngagement / dayPosts.length).toFixed(1)), + bestPost, + }); + } + + // Find best days + const daysWithPosts = byDayOfWeek.filter(d => d.postCount > 0); + const bestDayImpressions = daysWithPosts.length > 0 + ? daysWithPosts.reduce((best, d) => d.avgImpressions > best.avgImpressions ? d : best).dayName + : "N/A"; + const bestDayEngagement = daysWithPosts.length > 0 + ? daysWithPosts.reduce((best, d) => d.avgEngagementRate > best.avgEngagementRate ? d : best).dayName + : "N/A"; + + // Date range + const sortedDates = posts.map(p => p.publishedDate).sort(); + const dateRange = posts.length > 0 + ? { from: sortedDates[0], to: sortedDates[sortedDates.length - 1] } + : { from: "", to: "" }; + + return { + generatedAt: new Date().toISOString(), + postsAnalyzed: posts.length, + dateRange, + byDayOfWeek, + bestDayImpressions, + bestDayEngagement, + }; +} diff --git a/plugins/linkedin-thought-leadership/scripts/analytics/src/reports/monthly.ts b/plugins/linkedin-thought-leadership/scripts/analytics/src/reports/monthly.ts new file mode 100644 index 0000000..571abdd --- /dev/null +++ b/plugins/linkedin-thought-leadership/scripts/analytics/src/reports/monthly.ts @@ -0,0 +1,117 @@ +import type { PostAnalytics, MonthlyReport } from "../models/types.js"; +import { loadAllPosts, loadMonthlyReport, saveMonthlyReport } from "../utils/storage.js"; +import { mean } from "../utils/stats.js"; +import { detectAlerts } from "../utils/alerts.js"; +import { getISOWeek } from "./weekly.js"; + +/** + * Get previous month string (e.g., "2026-03" → "2026-02") + */ +function getPreviousMonth(month: string): string { + const [year, m] = month.split("-").map(Number); + if (m === 1) return `${year - 1}-12`; + return `${year}-${String(m - 1).padStart(2, "0")}`; +} + +/** + * Generate a monthly report with optional MoM comparison. + * Saves the report to disk and returns it. + */ +export function generateMonthlyReport(root: string, month: string): MonthlyReport { + const allPosts = loadAllPosts(root); + const monthPosts = allPosts.filter(p => p.publishedDate.startsWith(month)); + + // Summary + const totalPosts = monthPosts.length; + const totalImpressions = monthPosts.reduce((s, p) => s + p.metrics.impressions, 0); + const totalReactions = monthPosts.reduce((s, p) => s + p.metrics.reactions, 0); + const totalComments = monthPosts.reduce((s, p) => s + p.metrics.comments, 0); + const totalShares = monthPosts.reduce((s, p) => s + p.metrics.shares, 0); + const totalClicks = monthPosts.reduce((s, p) => s + p.metrics.clicks, 0); + const avgEngagementRate = totalPosts > 0 + ? parseFloat(mean(monthPosts.map(p => p.metrics.engagementRate)).toFixed(2)) + : 0; + const avgImpressionsPerPost = totalPosts > 0 + ? Math.round(totalImpressions / totalPosts) + : 0; + + // Top performers (sorted by impressions desc) + const topPerformers = [...monthPosts] + .sort((a, b) => b.metrics.impressions - a.metrics.impressions) + .slice(0, 5); + + // Weekly breakdown + const weekBuckets = new Map(); + for (const post of monthPosts) { + const week = getISOWeek(new Date(post.publishedDate + "T00:00:00Z")); + if (!weekBuckets.has(week)) weekBuckets.set(week, []); + weekBuckets.get(week)!.push(post); + } + + const byWeek = Array.from(weekBuckets.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([week, posts]) => ({ + week, + postCount: posts.length, + avgImpressions: Math.round(mean(posts.map(p => p.metrics.impressions))), + avgEngagementRate: parseFloat(mean(posts.map(p => p.metrics.engagementRate)).toFixed(1)), + })); + + // MoM comparison + const prevMonth = getPreviousMonth(month); + const prevReport = loadMonthlyReport(root, prevMonth); + + let trends: MonthlyReport["trends"]; + if (prevReport && prevReport.summary.totalPosts > 0) { + const pctImpr = prevReport.summary.totalImpressions > 0 + ? parseFloat(((totalImpressions - prevReport.summary.totalImpressions) / prevReport.summary.totalImpressions * 100).toFixed(1)) + : null; + const pctEng = prevReport.summary.avgEngagementRate > 0 + ? parseFloat(((avgEngagementRate - prevReport.summary.avgEngagementRate) / prevReport.summary.avgEngagementRate * 100).toFixed(1)) + : null; + const pctPosts = prevReport.summary.totalPosts > 0 + ? parseFloat(((totalPosts - prevReport.summary.totalPosts) / prevReport.summary.totalPosts * 100).toFixed(1)) + : null; + + trends = { + comparedTo: prevMonth, + percentChange: { + impressions: pctImpr, + engagement: pctEng, + postCount: pctPosts, + }, + }; + } else { + trends = { + comparedTo: null, + percentChange: { impressions: null, engagement: null, postCount: null }, + }; + } + + // Alerts + const alerts = totalPosts > 0 ? detectAlerts(monthPosts, "impressions") : []; + + const report: MonthlyReport = { + month, + generatedAt: new Date().toISOString(), + summary: { + totalPosts, + totalImpressions, + totalReactions, + totalComments, + totalShares, + totalClicks, + avgEngagementRate, + avgImpressionsPerPost, + }, + topPerformers, + byWeek, + trends, + alerts, + }; + + // Save report + saveMonthlyReport(root, report); + + return report; +} diff --git a/plugins/linkedin-thought-leadership/scripts/analytics/src/utils/storage.ts b/plugins/linkedin-thought-leadership/scripts/analytics/src/utils/storage.ts index b76f88a..3572f3a 100644 --- a/plugins/linkedin-thought-leadership/scripts/analytics/src/utils/storage.ts +++ b/plugins/linkedin-thought-leadership/scripts/analytics/src/utils/storage.ts @@ -1,7 +1,7 @@ import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "node:fs"; import { join, resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import type { AnalyticsBatch, WeeklyReport, PostAnalytics } from "../models/types.js"; +import type { AnalyticsBatch, WeeklyReport, MonthlyReport, PostAnalytics } from "../models/types.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,7 +25,7 @@ export function getAnalyticsRoot(): string { * Ensure required subdirectories exist under analytics root */ export function ensureDirectories(root: string): void { - const directories = ["exports", "posts", "weekly-reports"]; + const directories = ["exports", "posts", "weekly-reports", "monthly-reports"]; if (!existsSync(root)) { mkdirSync(root, { recursive: true }); @@ -252,3 +252,39 @@ export function loadAllWeeklyReports(root: string): WeeklyReport[] { b.week.localeCompare(a.week) ); } + +/** + * Sanitize month string to only allow YYYY-MM format + */ +function sanitizeMonth(month: string): string { + if (!/^\d{4}-\d{2}$/.test(month)) { + throw new Error(`Invalid month format: ${month}. Expected YYYY-MM`); + } + return month; +} + +/** + * Save a monthly report to disk + */ +export function saveMonthlyReport(root: string, report: MonthlyReport): string { + ensureDirectories(root); + const reportsDir = join(root, "monthly-reports"); + const month = sanitizeMonth(report.month); + const filename = `${month}.json`; + const filepath = join(reportsDir, filename); + verifyPathWithinDirectory(filepath, reportsDir); + writeFileSync(filepath, JSON.stringify(report, null, 2), "utf-8"); + return filename; +} + +/** + * Load a specific monthly report by month identifier + */ +export function loadMonthlyReport(root: string, month: string): MonthlyReport | null { + month = sanitizeMonth(month); + const reportsDir = join(root, "monthly-reports"); + const filepath = join(reportsDir, `${month}.json`); + if (!existsSync(filepath)) return null; + const content = readFileSync(filepath, "utf-8"); + return JSON.parse(content) as MonthlyReport; +} diff --git a/plugins/linkedin-thought-leadership/scripts/analytics/tests/heatmap.test.ts b/plugins/linkedin-thought-leadership/scripts/analytics/tests/heatmap.test.ts new file mode 100644 index 0000000..6c9c7dd --- /dev/null +++ b/plugins/linkedin-thought-leadership/scripts/analytics/tests/heatmap.test.ts @@ -0,0 +1,113 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { generateHeatmap } from "../src/reports/heatmap.js"; +import type { PostAnalytics } from "../src/models/types.js"; + +function createPost(date: string, impressions: number, engagementRate: number): PostAnalytics { + return { + id: `post-${date}`, + title: `Post on ${date}`, + publishedDate: date, + metrics: { + impressions, + reactions: 10, + comments: 5, + shares: 2, + clicks: 3, + engagementRate, + }, + importedAt: "2026-04-01T00:00:00Z", + exportSource: "test.csv", + }; +} + +describe("generateHeatmap", () => { + // Verified days: 2026-04-06=Mon, 07=Tue, 08=Wed, 09=Thu, 12=Sun, 13=Mon, 14=Tue, 15=Wed + const posts: PostAnalytics[] = [ + createPost("2026-04-06", 1000, 3.0), // Monday + createPost("2026-04-07", 2000, 4.0), // Tuesday + createPost("2026-04-08", 1500, 3.5), // Wednesday + createPost("2026-04-13", 3000, 5.0), // Monday + createPost("2026-04-14", 2500, 4.5), // Tuesday + createPost("2026-04-12", 800, 2.0), // Sunday + ]; + + test("groups posts by day of week correctly", () => { + const report = generateHeatmap(posts); + const monday = report.byDayOfWeek.find(d => d.dayName === "Monday"); + const tuesday = report.byDayOfWeek.find(d => d.dayName === "Tuesday"); + const sunday = report.byDayOfWeek.find(d => d.dayName === "Sunday"); + + assert.equal(monday?.postCount, 2); + assert.equal(tuesday?.postCount, 2); + assert.equal(sunday?.postCount, 1); + }); + + test("calculates correct averages per day", () => { + const report = generateHeatmap(posts); + const monday = report.byDayOfWeek.find(d => d.dayName === "Monday")!; + const tuesday = report.byDayOfWeek.find(d => d.dayName === "Tuesday")!; + + assert.equal(monday.avgImpressions, 2000); // (1000+3000)/2 + assert.equal(tuesday.avgImpressions, 2250); // (2000+2500)/2 + assert.equal(monday.avgEngagementRate, 4.0); // (3.0+5.0)/2 + }); + + test("handles days with no posts", () => { + const report = generateHeatmap(posts); + const friday = report.byDayOfWeek.find(d => d.dayName === "Friday")!; + + assert.equal(friday.postCount, 0); + assert.equal(friday.avgImpressions, 0); + assert.equal(friday.avgEngagementRate, 0); + }); + + test("returns 7 entries ordered Mon-Sun", () => { + const report = generateHeatmap(posts); + assert.equal(report.byDayOfWeek.length, 7); + assert.equal(report.byDayOfWeek[0].dayName, "Monday"); + assert.equal(report.byDayOfWeek[6].dayName, "Sunday"); + assert.deepEqual( + report.byDayOfWeek.map(d => d.dayIndex), + [1, 2, 3, 4, 5, 6, 7] + ); + }); + + test("identifies best day for impressions", () => { + const report = generateHeatmap(posts); + assert.equal(report.bestDayImpressions, "Tuesday"); + }); + + test("identifies best day for engagement", () => { + const report = generateHeatmap(posts); + assert.equal(report.bestDayEngagement, "Tuesday"); // (4.0+4.5)/2 = 4.25 + }); + + test("sets correct postsAnalyzed count", () => { + const report = generateHeatmap(posts); + assert.equal(report.postsAnalyzed, 6); + }); + + test("handles empty post list", () => { + const report = generateHeatmap([]); + assert.equal(report.postsAnalyzed, 0); + assert.equal(report.byDayOfWeek.length, 7); + assert.equal(report.bestDayImpressions, "N/A"); + assert.equal(report.bestDayEngagement, "N/A"); + for (const day of report.byDayOfWeek) { + assert.equal(day.postCount, 0); + } + }); + + test("identifies best post per day", () => { + const report = generateHeatmap(posts); + const monday = report.byDayOfWeek.find(d => d.dayName === "Monday")!; + assert.equal(monday.bestPost?.publishedDate, "2026-04-13"); // 3000 impressions + }); + + test("calculates correct date range", () => { + const report = generateHeatmap(posts); + assert.equal(report.dateRange.from, "2026-04-06"); + assert.equal(report.dateRange.to, "2026-04-14"); + }); +}); diff --git a/plugins/linkedin-thought-leadership/scripts/analytics/tests/monthly.test.ts b/plugins/linkedin-thought-leadership/scripts/analytics/tests/monthly.test.ts new file mode 100644 index 0000000..3ca5f53 --- /dev/null +++ b/plugins/linkedin-thought-leadership/scripts/analytics/tests/monthly.test.ts @@ -0,0 +1,135 @@ +import { describe, test, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { generateMonthlyReport } from "../src/reports/monthly.js"; +import { saveBatch } from "../src/utils/storage.js"; +import type { PostAnalytics, AnalyticsBatch, MonthlyReport } from "../src/models/types.js"; + +function createPost(date: string, impressions: number, engagementRate: number): PostAnalytics { + return { + id: `post-${date}-${impressions}`, + title: `Post on ${date}`, + publishedDate: date, + metrics: { + impressions, + reactions: Math.round(impressions * 0.05), + comments: Math.round(impressions * 0.01), + shares: Math.round(impressions * 0.005), + clicks: Math.round(impressions * 0.02), + engagementRate, + }, + importedAt: "2026-04-01T00:00:00Z", + exportSource: "test.csv", + }; +} + +function createBatch(posts: PostAnalytics[]): AnalyticsBatch { + const dates = posts.map(p => p.publishedDate).sort(); + return { + batchId: "test-batch-" + Math.random().toString(36).slice(2, 10), + importedAt: "2026-04-01T00:00:00Z", + exportFilename: "test.csv", + dateRange: { from: dates[0], to: dates[dates.length - 1] }, + postCount: posts.length, + posts, + }; +} + +let tmpDir: string; + +function setupTestRoot(posts: PostAnalytics[]): string { + tmpDir = mkdtempSync(join(tmpdir(), "monthly-test-")); + for (const dir of ["exports", "posts", "weekly-reports", "monthly-reports"]) { + mkdirSync(join(tmpDir, dir), { recursive: true }); + } + if (posts.length > 0) { + saveBatch(tmpDir, createBatch(posts)); + } + return tmpDir; +} + +afterEach(() => { + if (tmpDir) rmSync(tmpDir, { recursive: true, force: true }); +}); + +describe("generateMonthlyReport", () => { + const marchPosts: PostAnalytics[] = [ + createPost("2026-03-03", 1000, 3.0), + createPost("2026-03-05", 2000, 4.0), + createPost("2026-03-10", 1500, 3.5), + createPost("2026-03-17", 3000, 5.0), + createPost("2026-03-24", 2500, 4.5), + ]; + + const febPosts: PostAnalytics[] = [ + createPost("2026-02-03", 800, 2.5), + createPost("2026-02-10", 1200, 3.0), + createPost("2026-02-17", 900, 2.8), + ]; + + test("filters posts to correct month", () => { + const root = setupTestRoot([...marchPosts, ...febPosts]); + const report = generateMonthlyReport(root, "2026-03"); + assert.equal(report.summary.totalPosts, 5); + }); + + test("calculates correct monthly totals", () => { + const root = setupTestRoot(marchPosts); + const report = generateMonthlyReport(root, "2026-03"); + assert.equal(report.summary.totalImpressions, 10000); // 1000+2000+1500+3000+2500 + assert.equal(report.summary.totalPosts, 5); + assert.equal(report.summary.avgImpressionsPerPost, 2000); + }); + + test("generates weekly breakdown within month", () => { + const root = setupTestRoot(marchPosts); + const report = generateMonthlyReport(root, "2026-03"); + assert.ok(report.byWeek.length > 0); + const totalPostsInWeeks = report.byWeek.reduce((sum, w) => sum + w.postCount, 0); + assert.equal(totalPostsInWeeks, 5); + }); + + test("calculates MoM deltas when previous month exists", () => { + const root = setupTestRoot([...febPosts, ...marchPosts]); + // First generate February report so it exists for comparison + generateMonthlyReport(root, "2026-02"); + const report = generateMonthlyReport(root, "2026-03"); + + assert.notEqual(report.trends.comparedTo, null); + assert.equal(report.trends.comparedTo, "2026-02"); + assert.notEqual(report.trends.percentChange.impressions, null); + assert.notEqual(report.trends.percentChange.postCount, null); + }); + + test("handles no previous month data", () => { + const root = setupTestRoot(marchPosts); + const report = generateMonthlyReport(root, "2026-03"); + assert.equal(report.trends.comparedTo, null); + assert.equal(report.trends.percentChange.impressions, null); + assert.equal(report.trends.percentChange.engagement, null); + }); + + test("handles month with no posts", () => { + const root = setupTestRoot(marchPosts); + const report = generateMonthlyReport(root, "2026-01"); + assert.equal(report.summary.totalPosts, 0); + assert.equal(report.summary.totalImpressions, 0); + assert.equal(report.summary.avgImpressionsPerPost, 0); + assert.equal(report.byWeek.length, 0); + }); + + test("identifies top performers", () => { + const root = setupTestRoot(marchPosts); + const report = generateMonthlyReport(root, "2026-03"); + assert.ok(report.topPerformers.length > 0); + assert.equal(report.topPerformers[0].metrics.impressions, 3000); + }); + + test("sets correct month field", () => { + const root = setupTestRoot(marchPosts); + const report = generateMonthlyReport(root, "2026-03"); + assert.equal(report.month, "2026-03"); + }); +}); diff --git a/plugins/linkedin-thought-leadership/skills/linkedin-thought-leadership/SKILL.md b/plugins/linkedin-thought-leadership/skills/linkedin-thought-leadership/SKILL.md index e64e480..4b16072 100644 --- a/plugins/linkedin-thought-leadership/skills/linkedin-thought-leadership/SKILL.md +++ b/plugins/linkedin-thought-leadership/skills/linkedin-thought-leadership/SKILL.md @@ -34,6 +34,7 @@ This plugin uses **6 focused skills**. This main skill contains shared knowledge | User Intent | Route To | |-------------|----------| +| "Just installed" / "Walk me through" | `/linkedin:onboarding` | | "Set up plugin" | `/linkedin:setup` | | "Personalize" | `/linkedin:setup` | | "Improve personalization" | `/linkedin:setup` | @@ -115,6 +116,7 @@ These rules apply to ALL content created by any skill or command: | Command | Purpose | |---------|---------| | `/linkedin` | Router -- shows status line + command menu | +| `/linkedin:onboarding` | Multi-step onboarding wizard (profile → setup → first-post) | | `/linkedin:first-post` | First-post accelerator (zero to published in 10 min) | | `/linkedin:setup` | Guided setup to populate asset templates with real data | | `/linkedin:react` | URL-to-post pipeline -- react to articles, news, research | diff --git a/plugins/linkedin-thought-leadership/skills/linkedin-voice/SKILL.md b/plugins/linkedin-thought-leadership/skills/linkedin-voice/SKILL.md index 78784df..2cf6920 100644 --- a/plugins/linkedin-thought-leadership/skills/linkedin-voice/SKILL.md +++ b/plugins/linkedin-thought-leadership/skills/linkedin-voice/SKILL.md @@ -109,7 +109,7 @@ Transform each role with impact statements, not task lists: WHO you help + RESULT you deliver -Strong: "Helping [target audience] achieve [specific result] | [Your Role] @ [Your Organization]" +Strong: "Helping public sector leaders implement AI that actually works | AI Advisor @ Statens vegvesen" ---