linkedin-studio/commands/report.md
Kjell Tore Guttormsen e2ad190dda feat(linkedin-studio): N17 — baseline-motor (median + variansbånd + minimum-N-refusal) [skip-docs]
Every reading now leads with "vs your own baseline", and no verdict is given
when N is too small to carry one.

- stats.ts: median + medianAbsoluteDeviation (robust pair; mean/stddev stay for
  the alert engine, which wants outlier sensitivity), rollingBaseline with a
  10-post positional window, median ± 1·MAD band floored at 0, and a typed
  insufficient-data refusal below MIN_BASELINE_N=5. readAgainstBaseline returns
  above/within/below-band, or no-verdict when the baseline was refused.
- baselineByGroup + buildBaselineBlock: per-format/per-pillar baselines, each
  judged on its own N; the reported period is excluded from its own baseline and
  compared on its median, not its mean.
- queue-join.ts (new): read-only date join supplying format/pillar from the post
  queue. Every ambiguity resolves to unlabelled, an entry labels at most one
  post, and a missing/broken queue degrades to no labels.
- weekly/monthly reports attach the block unconditionally (refusal included);
  optional in the types, so pre-N17 reports load unchanged.
- CLI: report output leads with the baseline; new `baseline [--by format|pillar]`
  verb with coverage reporting.
- report.md leads with baseline framing and prints the code's reading rather
  than judging the band by eye; WoW loses to the baseline on disagreement.
  analyze.md Step 2a tests whether the drop is real before diagnosing it.

TDD: 58 analytics tests written red first (144 -> 202). test-runner Section 16x,
23 unconditional checks + self-test (247 -> 270; anti-erosion floor 228 -> 251).
tsc clean. All suites green: trends 300, brain 134, editions 72,
specifics-bank 45, contract-gate 33, hooks 191, tests 35, render 60.

Also closes the OKF phase-4 scope follow-up in docs/okf-ingestion/plan.md §8
(coord round 2026-07-25): phase 4 tracks the contract, parse is in scope, and
our claim on read_concept/navigate_bundle is withdrawn as unnecessary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
2026-07-25 20:50:45 +02:00

29 KiB
Raw Blame History

name description allowed-tools
linkedin:report Generate a weekly performance report from imported LinkedIn analytics data. Shows key metrics, top performers, trends, and actionable alerts. Use when the user wants to review their LinkedIn performance. Triggers on: "weekly report", "performance report", "generate report", "show my stats", "analytics report", "how did I do", "LinkedIn performance".
Bash
Read
Glob
Write
AskUserQuestion
Task

LinkedIn Analytics Weekly Report

You are a LinkedIn analytics performance reporter. Generate actionable weekly performance reports from imported analytics data.

Reference

For data format details and directory structure, see assets/analytics/README.md.

Step 1: Check for Imported Data

First, verify that analytics data exists:

ls -1 ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/ 2>/dev/null | grep -E '\.json$' | head -10

If no JSON files exist, tell the user:

No analytics data found.

You need to import your LinkedIn analytics first:

  1. Run /linkedin:import to import CSV data
  2. Then come back to generate reports

Step 1b: Ensure analytics CLI dependencies (first run)

The analytics CLI runs under tsx and depends on csv-parse. Both live in scripts/analytics/node_modules/, which is gitignored — so on a fresh clone they are absent and the CLI calls below would crash with ERR_MODULE_NOT_FOUND. Install them once (idempotent — a fast no-op when already present):

cd "${CLAUDE_PLUGIN_ROOT}/scripts/analytics" && npm install --silent

The CLI calls below invoke the locally-installed tsx by its absolute node_modules/.bin/tsx path (not bare tsx/node --import tsx), so they resolve from whatever working directory the command runs in — but only after the install above has created that binary.

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 2c. 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
- "last week" - Previous ISO week
- Specific week: "2026-W03", "2025-W52", etc.
- "latest" - Most recent week with data

Enter your choice:

ISO Week Format: YYYY-WXX (e.g., 2026-W05 for week 5 of 2026)

To get current ISO week:

date +%Y-W%V

Step 2b: Monthly Report

If the user chose monthly:

"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --month <YYYY-MM>

Read the generated JSON from ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/monthly-reports/<YYYY-MM>.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:

"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/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:

"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --week <YYYY-WXX>

Example:

"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --week 2026-W05

The CLI will generate:

  • ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/weekly-reports/YYYY-WXX.json - Structured report data

Step 4: Read Generated Report Data

Read the generated JSON report:

cat ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/weekly-reports/<YYYY-WXX>.json

The report contains:

  • week: ISO week identifier
  • generatedAt: ISO timestamp when the report was generated
  • summary: Totals and averages — totalPosts, totalImpressions, totalReactions, totalComments, totalShares, totalClicks, optional totalSaves (manual entry only), optional avgOutOfNetworkPct (manual entry only — the impressions-weighted out-of-network share), avgEngagementRate, avgImpressionsPerPost
  • baseline: The operator's own normal, from posts published before this week — window, required, historyBefore, plus impressions and engagementRate (each either {status: "ok", n, median, mad, band} or {status: "insufficient-data", n, required, reason}), and period carrying this week's own median per metric with a reading of above-band / within-band / below-band / no-verdict. This is what the report leads with (Step 6).
  • topPerformers / underperformers: Best / weakest posts by engagement rate
  • trends: Week-over-week change — impressionsTrend, engagementTrend, comparedTo, percentChange
  • alerts: Anomalies and significant events (intra-week + week-over-week)

Week-over-week vs baseline — which to trust. trends.percentChange compares two adjacent weeks, so at two or three posts a week it swings wildly on sample composition alone; baseline compares against the operator's own spread. When the two disagree — a +38% week that reads within-bandthe baseline wins, and say so explicitly: the percentage is real arithmetic on a sample too small to carry a conclusion.

Step 4b: Per-format and per-pillar baselines (optional, when the queue has labels)

Format and pillar are not analytics data — they live in the post queue — so a separate surface joins them:

"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" baseline

It prints an overall baseline plus one per format and per pillar, and reports its own coverage (how many posts it could not label). Use it to answer "is carousel actually working for me?" — but respect the per-group refusals: each group is judged on its own N, so a format used twice gets no verdict even when the overall history is long. That refusal is the honest answer to a question the operator will still want answered; do not fill the gap with the overall baseline as a proxy for the group.

Skip this step when the join reports no labels at all — say the grouped view needs queue entries rather than presenting an empty table.

Step 5: Run Trend Analysis

Get additional context with trend analysis:

"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period month --metric impressions

This provides:

  • Trend direction (up/down/stable)
  • Percentage changes
  • Pattern detection (volatility, consistent growth, etc.)

Step 5b: Trend Analysis Deep-Dive

After the initial trend data, automatically run trend analysis for the key metrics:

Run trends CLI for key metrics:

"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" \
  trends --period month --metric impressions

"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" \
  trends --period month --metric engagementRate

Present trend summary as a 4-week comparison table:

### Trend Analysis (Last 4 Weeks)

| Metric | W-4 | W-3 | W-2 | W-1 (Current) | Trend |
|--------|-----|-----|-----|----------------|-------|
| Avg Impressions | X | X | X | X | ↑/↓/→ |
| Avg Engagement Rate | X% | X% | X% | X% | ↑/↓/→ |
| Posts Published | X | X | X | X | ↑/↓/→ |
| Best Format | ... | ... | ... | ... | — |

Trend interpretation rules:

  • ↑ Upward trend (>10% increase over 4 weeks): Highlight what's working
  • ↓ Downward trend (>10% decrease): Flag for strategy review
  • → Stable (within ±10%): Note consistency
  • If engagement rate is down but impressions up: Content reach expanding but resonance declining — consider revisiting hooks and CTAs
  • If engagement rate is up but impressions down: Niche audience engaged but reach limited — consider format diversification or posting time adjustment
  • If both declining: Possible algorithm signal change or content fatigue — review algorithm-signals-reference for latest penalties
  • If both growing: Strong momentum — maintain current strategy and document what's working

Construct the 4-week table by reading available weekly report files:

ls ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/weekly-reports/*.json 2>/dev/null | sort | tail -4

Read each file and extract the summary metrics to populate the table columns.

Step 5c: Alert Detection

Automatically flag these conditions based on the report data and trend analysis:

Performance Alerts:

  • 🔴 Critical: Engagement rate below 2% for 2+ consecutive weeks
  • 🔴 Critical: Zero posts in a week (streak broken)
  • 🟡 Warning: Impressions dropped >30% week-over-week
  • 🟡 Warning: Comment count below average for 2+ weeks
  • 🟢 Positive: New personal best in any metric
  • 🟢 Positive: Consistent posting streak maintained (7+ days)

Algorithm Alerts (based on algorithm-signals-reference):

  • 🔴 Format stagnation: Same format used >80% of posts (algorithm favors format variety — see algorithm-signals-reference)
  • 🟡 Posting time drift: Publishing outside optimal window (Tue-Thu, 7-9 AM CET for Nordic audience — see posting time windows reference)
  • 🟡 Hook length violation: Posts with hooks >140 chars underperforming (>140 chars truncated on mobile "see more")
  • 🟢 Engagement velocity improving: First-hour engagement trending up (15+ engagements in first hour unlocks 2nd/3rd degree distribution)

Surface the alerts the report already computed. The weekly-report JSON's alerts[] is generated by the CLI itself — intra-week anomaly detection across the week's posts (detectAlerts) plus a week-over-week comparison (detectWeeklyAlerts) that fires once the previous week's report exists. There is no separate baselines file to consult; read alerts[] straight from the report you generated in Step 3:

cat ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/weekly-reports/<YYYY-WXX>.json 2>/dev/null \
  | python3 -c "import json,sys; [print(a['severity'].upper(), '—', a['message']) for a in json.load(sys.stdin).get('alerts', [])]"

Each alert carries a severity (critical / warning / info); map it to an icon (🔴 / 🟡 / 🟢) and pair it with an action below. Week-over-week alerts appear only after a prior week's report has been generated.

Present alerts as:

### Alerts & Recommendations

🔴 **Critical: Engagement rate declining**
Your engagement rate has dropped from 4.2% to 2.8% over the last 3 weeks.
→ **Action:** Review recent post hooks. Consider more provocative angles or questions.
→ **Reference:** Hook length should be <140 chars. In the engagement order (see `references/algorithm-signals-reference.md`), saves rank above shares, then quality comments, then reactions. Saves are visible in your native LinkedIn post analytics (count-only, ~Sept 2025 onward) but (as of 2026-05) there is no self-serve API to pull them — so this tool does not auto-track saves; read them in LinkedIn directly. Dwell time is internal to LinkedIn for organic posts (not exposed anywhere this tool can read).

🟢 **Positive: New impression record**
Your post on [topic] achieved 12,500 impressions — a personal best!
→ **Action:** Analyze what made this post succeed. Consider a follow-up post.
→ **Reference:** First-hour velocity of 15+ engagements unlocks broader distribution.

🟡 **Warning: Format stagnation detected**
80%+ of your recent posts are text-only. Documents/carousels are the top organic format and tend to reach further (no reliable multiplier).
→ **Action:** Try a carousel or multi-image post this week for format diversification.

Step 6: Present Formatted Report

The report LEADS with the operator's own baseline, and the absolute numbers come second. At a normal publishing cadence a single week holds two or three posts, so an absolute total answers no question on its own — "1,240 impressions" is only information once you know whether that is ordinary for this operator. Present the baseline block first, every time, and never reorder these two sections.

Do not compute the verdict yourself. The report JSON's baseline block already carries it, decided in code: baseline.period.<metric>.reading is one of above-band / within-band / below-band / no-verdict. Print what it says. Judging by eye whether a number sits inside the band is the over-read this block exists to prevent — a value 7% above the median looks like a trend and is not one.

Format the data into a readable report using this template:

# LinkedIn Performance Report
## Week {week}

### 📐 Vs Your Own Baseline

Baseline = your last {baseline.window} posts published **before** this {week/month}
(n={baseline.impressions.n}). Median and normal range, not mean — one viral post
must not redefine "normal".

| Metric | This week (median) | Your baseline | Normal range | Reads as |
|--------|--------------------|---------------|--------------|----------|
| Impressions/post | {baseline.period.impressions.median} | {baseline.impressions.median} | {band.low}{band.high} | {reading} |
| Engagement rate | {baseline.period.engagementRate.median}% | {baseline.engagementRate.median}% | {band.low}{band.high}% | {reading} |

Render `reading` in words, not as the raw token:
- `above-band` → **above your normal range** — a real move up, worth explaining
- `below-band` → **below your normal range** — a real move down, worth diagnosing
- `within-band` → **inside your normal range** — normal variation, not a trend.
  Say this plainly and do not dress it up as momentum in either direction.
- `no-verdict` → see the refusal rule below

**When `baseline.<metric>.status` is `insufficient-data`: refuse the verdict.** Print
`baseline.<metric>.reason` verbatim and state that this {week/month}'s numbers are
reported **without** a verdict. Do not substitute a week-over-week percentage as a
stand-in, do not call it a trend, and do not soften the refusal into a hedge like
"early signs suggest" — with fewer than {baseline.required} prior posts there is no
signal to be early about. The absolute numbers below are still shown; only the
verdict is withheld. An operator who has just started publishing gets this refusal
for several weeks, and that is the correct output, not a failure of the report.

**If the report JSON has no `baseline` key at all**, it was generated before
baselines existed — say so, and offer to re-run Step 3 rather than reading the
absolutes as if they had been framed.

### 📊 Key Metrics

| Metric | Total | Average per Post | vs. Last Week |
|--------|-------|------------------|---------------|
| Impressions | {total} | {avg} | {trend} |
| Reactions | {total} | {avg} | {trend} |
| Comments | {total} | {avg} | {trend} |
| Shares | {total} | {avg} | {trend} |
| Engagement Rate | - | {rate}% | {trend} |

**Posts published:** {summary.totalPosts}
**Engagement rate:** {summary.avgEngagementRate}%

### 🌐 Reach Split (only when `summary.avgOutOfNetworkPct` is present)

| Half | Share of impressions | Reads as |
|------|----------------------|----------|
| Out-of-network | {summary.avgOutOfNetworkPct}% | Acquisition — the post reached people who do not follow you |
| In-network | {100 - summary.avgOutOfNetworkPct}% | Resonance — it landed with the audience you already have |

State the coverage honestly: the share is impressions-weighted across **only the
posts that carried a manual reading**. If the field is absent, say the split is
**not entered for this period** — never estimate it, and never print 0%.

Read the two halves against the week's goal, not as a score:
- **Out-of-network up, engagement rate flat/down** — the algorithm distributed it to
  strangers who did not act. Reach without resonance; the hook traveled, the body did not.
- **In-network high, out-of-network low** — deepening, not growing. Fine for a trust play,
  a problem if the week's goal was audience growth.
- **Both up** — the topic has pull beyond your circle. This is the pattern worth repeating.

### 🏆 Top Performers

**Most Impressions:**
"{post.content}" - {impressions} impressions ({date})

**Most Engaged:**
"{post.content}" - {engagementRate}% engagement ({date})

**Most Shared:**
"{post.content}" - {shares} shares ({date})

### 🚨 Alerts & Insights

{List any anomalies, viral posts, or underperformers}

### 📈 Trend Analysis (Last 4 Weeks)

{Trend summary from trends CLI output}
- Impressions: {trend direction} ({percentage change})
- Engagement: {trend direction} ({percentage change})
- Publishing frequency: {pattern}

### 💡 Recommendations

{Generate 2-3 actionable recommendations based on the data}

Example recommendations:
- "Your posts on [topic] are performing 40% above average. Consider posting more on this topic."
- "Engagement drops significantly on [day]. Try shifting your posting schedule."
- "Posts with [format] are getting 2x more shares. Experiment with this format more."

Step 7: Generate Actionable Recommendations

Delegate interpretation to the analytics-interpreter agent (report mode) — invoke it via Task with subagent_type: linkedin-studio:analytics-interpreter (foreground, from this command layer), passing the generated report data; it surfaces the patterns behind the numbers. Based on the report data and the agent's reading, provide 2-3 specific, actionable recommendations:

Framework for recommendations:

  1. What's working? - Double down on successful patterns

    • Topic clusters with high engagement
    • Format types with high shares
    • Posting times with high reach
  2. What's not working? - Diagnose underperformance

    • Topics with low impressions
    • Posts with engagement below baseline
    • Timing issues
  3. What to test next? - Experiments to run

    • New formats for top topics
    • Different posting times
    • Content angles that worked elsewhere

Example recommendations:

💡 Recommendations for Next Week:

1. **Double down on AI content**: Your 3 posts about AI agents averaged 2,400 impressions (vs. 1,200 baseline). Plan 2 more AI-focused posts this week.

2. **Fix Tuesday underperformance**: Tuesday posts got 40% fewer impressions than other days. Try posting at 8am instead of 12pm.

3. **Test carousel format**: Your one carousel got 3x more shares than text posts. Create a carousel for your top-performing topic this week.

Step 7b: Persist them as do-next directives (the measurement→creation contract)

A recommendation that only lands in chat dies there — the next post is drafted in a fresh session that never saw it. Persist the 23 recommendations above as do-next directives, so /linkedin:post, /linkedin:quick, /linkedin:batch, /linkedin:create and /linkedin:newsletter read them at their Step 0 and the next piece is actually shaped by this report. This step is not optional when a report produced recommendations.

Each directive is an imperative the next draft can act on plus an evidence pointer — the measured number it came from, so the create surface can tell a directive backed by 12 posts from one backed by 2:

node --input-type=module -e "
import { writeState, recordDoNext } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/state-updater.mjs';
writeState(content => recordDoNext(content, {
  recordDate: 'YYYY-MM-DD',
  source: 'report',
  directives: [
    { directive: 'Plan 2 AI-agent posts this week', evidence: 'weekly 2026-W22: 3 AI posts avg 2400 impressions vs 1200 baseline (n=12)' },
    { directive: 'Move Tuesday posts to 08:00', evidence: 'weekly 2026-W22: Tuesday -40% impressions' }
  ]
}));
"

A reach reading belongs in this channel too. When avgOutOfNetworkPct is present, the split is a directive about what to write next, not a number to admire — so persist it here rather than leaving it in the report:

  • Out-of-network high on one topic → 'Write the next post on <topic> — it travels past your network', evidence 'weekly 2026-W23: <topic> 58% out-of-network vs 31% weighted average'
  • Out-of-network up but engagement rate down → 'Keep the <topic> hook, rebuild the body — strangers saw it and did not act'
  • Out-of-network low across the week → 'Next post targets a problem non-followers search for, not an in-circle reference'

Never persist a reach directive when the field is absent — an unentered split is unknown, and a directive with no measured evidence pointer is exactly what this channel exists to prevent.

Lifetime (do not hand-manage it): recordDoNext replaces this source's own previous rows on every write and drops any row older than 60 days, so the section stays a live steering signal rather than a growing backlog. Directives from other sources (ab-test, analyze, 48h-monitor) are left alone — never clear the section by hand.

Then confirm in one line what was persisted, e.g. "2 do-next directives written to state — the next post/newsletter will read them at Step 0."

Step 7c: Offer a baseline update to engagement-patterns.md (operator-gated)

The do-next directives above steer the next piece. This step maintains the standing baseline every other surface reads: ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/audience-insights/engagement-patterns.md — the file analytics-interpreter, strategy-advisor and /linkedin:audit treat as the user's tracked truth about their own audience. It goes stale silently: nothing in the pipeline writes it, so it keeps asserting last quarter's best time and topics while the reports say otherwise.

So after a report, offer a diff — never write it silently:

  1. Read the baseline file. If it is missing or still all [Day]/[Topic] placeholders, say so and offer to seed it from this report instead of diffing.
  2. Compare only what this report actually measured — best posting windows, top-performing topics, format performance, and (when entered) the reach split. Leave every other section alone.
  3. Present each proposed change as old → new with the evidence, e.g.:
Proposed baseline updates (from weekly 2026-W23, n=11 posts):

1. Primary posting window
   old: Tuesday at 12:00 — avg 1,200 impressions
   new: Tuesday at 08:00 — avg 2,050 impressions (4 posts over 3 weeks)

2. Top-performing topic
   old: (empty)
   new: agent orchestration — avg 4.1% engagement, 58% out-of-network (3 posts)

3. Format performance
   old: carousel — untracked
   new: carousel — 3x shares vs text (1 post — thin, flagged as provisional)
  1. Ask for an explicit go with AskUserQuestion — options: Apply all · Apply selected · Skip. Skipping is a normal outcome; a thin sample is a reason to skip.
  2. Only on an explicit go, edit the file — change the affected lines and append to that section's **Update Log:** with the date and what the number came from. Never rewrite sections the report did not measure, and never delete the user's own annotations.
  3. If a proposed change rests on 12 posts, label it provisional in the diff and say plainly that it is not yet a pattern. Do not let a single post rewrite a baseline.

Write only to the per-user data dir path above — never to the plugin's shipped seed (engagement-patterns-template.md and its placeholder sibling), which is the template the data dir was initialized from, not the user's data.

Step 8: Offer Deep Dive Options

After presenting the report, ask:

Would you like to dive deeper into any area?

1. Analyze specific posts in detail
2. Compare this week to previous weeks
3. Run trend analysis for other metrics (comments, shares)
4. Export report as markdown file
5. Done - I have what I need

Use AskUserQuestion for selection.

Deep Dive: Trend Analysis for Other Metrics

If user wants more trend analysis:

# Analyze comments trend
"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period month --metric comments

# Analyze shares trend
"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period month --metric shares

# Analyze engagement rate trend
"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period month --metric engagementRate

Present additional insights from these trends.

Deep Dive: Post Analysis

If user wants to analyze specific posts:

Read the weekly post data directly:

for f in ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/*.json; do
  jq '.posts[] | select(.title | contains("search term"))' "$f"
done

Show detailed metrics for that post and suggest what made it perform well/poorly.

Error Handling

If report generation fails:

  1. Week not found: No data imported for that week

    • List available weeks: ls ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/
    • Suggest importing data for that week
  2. No posts in week: Week file exists but is empty

    • Confirm user didn't post that week
    • Suggest checking import data
  3. CLI error: Technical failure

    • Show error message
    • Check file permissions
    • On ERR_MODULE_NOT_FOUND (missing tsx/csv-parse on a fresh clone), install the analytics CLI dependencies: cd "${CLAUDE_PLUGIN_ROOT}/scripts/analytics" && npm install --silent

State Integration

After generating report, optionally update user's posting state:

Read ~/.claude/linkedin-studio.local.md and suggest:

  • If week had 0 posts: "Streak broken - consider posting this week to restart"
  • If week hit goal: "Goal achieved! Maintaining consistency."
  • If week exceeded goal: "Exceeding goal - strong momentum!"

Reference Files

Reports use data from:

  • ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/YYYY-MM-DD-<shortid>.json - Raw imported batches (one file per import; rows live under .posts[])
  • ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/weekly-reports/YYYY-WXX.json - Computed weekly report
  • ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/monthly-reports/YYYY-MM.json - Computed monthly report

Step 8b: Export Options

If the user chooses option 4 ("Export report as markdown file") from the deep dive menu:

Generate and save a clean markdown report:

  1. Read the JSON report data:
cat ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/weekly-reports/<YYYY-WXX>.json
  1. Format the data using this template and write to file:

Save to: ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/weekly-reports/YYYY-WXX-report.md

# LinkedIn Performance Report — Week YYYY-WXX

**Generated:** YYYY-MM-DD
**Posts analyzed:** X

## Key Metrics

| Metric | Total | Avg per Post | vs. Last Week |
|--------|-------|--------------|---------------|
| Impressions | X | X | ↑/↓/→ X% |
| Reactions | X | X | ↑/↓/→ X% |
| Comments | X | X | ↑/↓/→ X% |
| Shares | X | X | ↑/↓/→ X% |
| Engagement Rate | — | X% | ↑/↓/→ X% |

## Trend Analysis (Last 4 Weeks)

| Metric | W-4 | W-3 | W-2 | W-1 (Current) | Trend |
|--------|-----|-----|-----|----------------|-------|
| Avg Impressions | X | X | X | X | ↑/↓/→ |
| Avg Engagement Rate | X% | X% | X% | X% | ↑/↓/→ |
| Posts Published | X | X | X | X | ↑/↓/→ |

## Alerts

[List all alerts from Step 5c with severity icons and actions]

## Top Performers

### Most Impressions
"[post hook text]" — X impressions (YYYY-MM-DD)

### Most Engaged
"[post hook text]" — X% engagement rate (YYYY-MM-DD)

### Most Shared
"[post hook text]" — X shares (YYYY-MM-DD)

## Recommendations

1. [Actionable recommendation based on data]
2. [Actionable recommendation based on data]
3. [Actionable recommendation based on data]

---
*Generated by linkedin-studio plugin*

Important notes:

  • The ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/ directory is gitignored — exported reports contain personal analytics data and should not be committed
  • Use the -report.md suffix to distinguish from the JSON data files (e.g., 2026-W05-report.md vs 2026-W05.json)
  • Include all sections: metrics, trends, alerts, top performers, and recommendations for a complete standalone document

After saving, confirm to the user:

Report exported to: ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/weekly-reports/YYYY-WXX-report.md

Note: This file is in your gitignored analytics directory — it won't be committed to the repository.