linkedin-studio/commands/import.md
Kjell Tore Guttormsen e7276cf8bb fix(linkedin-studio): N20 — CLAUDE_PLUGIN_ROOT-normalisering av load-bearing ref-stier [skip-docs]
81 load-bearing stier i 16 kommandofiler pinnet til ${CLAUDE_PLUGIN_ROOT}/.
Rene prefikser: 81 inn / 81 ut, ingen linje lagt til eller fjernet.

FASIT-VERIFISERING (premiss-sjekk mot BASELINE, ikke bare HEAD): planens
68 linjer / 17 filer er RIKTIG mot sin egen baseline 882f6ee. HEAD ga 73/18 —
altså ekte kode-drift siden baseline (newsletter.md 11→15, calendar.md 0→1),
ikke en telle-feil som i N19.

SCOPE UTVIDET ETTER GO: planens grep (references|hooks/scripts|scripts) har en
blindsone — skills/ assets/ config/ render/ agents/ commands/ er samme defekt-
klasse og allerede anerkjent som pin-verdig av mønsteret (audit.md:24 og
competitive.md:22 pinner skills/, batch.md:214 og pipeline.md:211 pinner
assets/). Under planens scope kunne dens EGEN verifisering ikke passere:
quick.md:229-230 ville stått upinnet rett over en pinnet linje, og setup.md
(3 load-bearing Read, 0 pins) var ikke i settet i det hele tatt.
Full defektklasse: 117 linjer / 19 filer → 81 pinnet, 36 bevisst urørt.

KLASSIFISERINGSREGEL (utledet av mønsteret, ikke oppfunnet):
- PIN = stien skal åpnes/kjøres/skrives — Read/Reference/See-pekere,
  ref-fil-lister, Bash/node/npm, edit-mål.
- PROSA = stien står som sitat bak en påstand, eller som beskrivelse av
  oppførsel/plassering. Ingen inline «(see …)» er pinnet noe sted i repoet
  (0 av 25 filer) — den grensen er arvet, ikke satt her.

36 BEVISST URØRTE, tre klasser:
1. Inline sitat/attribusjon (17): carousel.md:37 · pipeline.md:70,75 ·
   newsletter.md:999,1082,1211,1433 · report.md:273 · video.md:69 ·
   react.md:115 · calendar.md:87 · profile.md:28,34 · firsthour.md:112 ·
   monetize.md:343,492 · outreach.md:922
2. Beskrivelse av oppførsel/plassering (10): newsletter.md:50,1097,1205,1694,
   1744,1944 · report.md:45 · linkedin.md:218 · import.md:121,200
3. newsletter.md fase-tabellens Tooling-kolonne (9): :100,102,105,106,111,112,
   113,115,116 — tabellen er et register, og hvert steg pinner sitt eget
   faktiske kall i brødteksten.

S13-LINTEN (M0 data-dir): alle assets/-stiene som ble pinnet står eksplisitt i
lintens NEGATIVE13-unntaksliste, og lintens egen kommentar (test-runner.sh:552)
sier at shipped read-only assets SKAL bære ${CLAUDE_PLUGIN_ROOT}.

VERIFISERING: 76 unike pinnede stier hentet ut og resolvet med cwd=/tmp —
75 finnes. Den ene som ikke gjør det, config/personas.local.md, er en
gitignored valgfri fallback («else personas.template.md») som var pinnet på
newsletter.md:205 før denne endringen. Re-grep: gjenstående 36 er utelukkende
prosa, listet over.

Alle ti suiter grønne, alle floors uendret: test-runner 270/0 (269 assertions
>= floor 251) · trends 300/0 · analytics 202/0 · hooks 191/0 · brain 134/0 ·
editions 72/0 · render 63/0 · specifics-bank 45/0 · tests 35/0 ·
contract-gate 33/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M31cw43gqiSSDrKzcwUi7L
2026-07-31 17:21:22 +02:00

12 KiB
Raw Blame History

name description allowed-tools
linkedin:import Import a LinkedIn analytics CSV export into the structured analytics system. Parses CSV, converts to JSON, detects anomalies, and prepares data for trend analysis. Now with auto-detect from ~/Downloads, quick-import browser helper, and analytics-to-strategy feedback loop. Use when the user wants to import analytics data from LinkedIn. Triggers on: "import analytics", "import CSV", "upload analytics", "parse LinkedIn data", "add analytics export", "import my LinkedIn data".
Bash
Read
Glob
Write
AskUserQuestion

LinkedIn Analytics Import Workflow

You are a LinkedIn analytics data import assistant. Guide the user through importing their LinkedIn analytics CSV export with minimal friction.

Reference

For data format details and directory structure, see ${CLAUDE_PLUGIN_ROOT}/assets/analytics/README.md.

Why CSV (as of 2026-07). Post-level analytics via LinkedIn's API is partner-gated (vetted Community Management app + verified org + Page) and not self-serve for a personal profile, so the CSV export is the practical floor. Saves are visible in native post analytics (count-only) but have no self-serve API pull; the in-network/out-of-network reach split is visible natively too (Discovery, under impressions — rolling out globally since June 2026) but as percentages, and is absent from the CSV, with export status unverified; dwell is internal-only for organic posts. Saves and the reach share are therefore optional manual columns the user adds to the CSV (Saves, Out-of-network / In-network) — see ${CLAUDE_PLUGIN_ROOT}/assets/analytics/README.md for the entry rules, and the README boundaries for the full data boundary.

Step 1: Check for CSV Files in Exports Directory

First, check if any CSV files exist in the exports directory:

ls -lh ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/exports/*.csv 2>/dev/null || echo "No CSV files found"

If files found: Skip to Step 3.

Step 1b: Auto-Detect from ~/Downloads

If no files in exports directory, scan ~/Downloads/ for recent LinkedIn CSV files:

find ~/Downloads -maxdepth 1 -name "*.csv" -mtime -14 -type f 2>/dev/null | sort -t/ -k$(echo ~/Downloads/x | tr '/' '\n' | wc -l) | head -10

Filter results for LinkedIn-looking files (filenames containing 'linkedin', 'analytics', 'content', 'export', or any CSV modified in the last 24 hours).

If matching files found, present them using AskUserQuestion:

Options:

  • Import specific file — Select one of the detected files
  • Import all — Import all matching CSV files
  • Quick-import — Open LinkedIn Analytics in browser and auto-detect download
  • Skip — Show manual instructions instead

On file selection, copy the file to the exports directory:

cp "<selected-file>" ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/exports/

Then continue to Step 4.

Step 2: If No Files Found Anywhere

If no CSV files exist in exports or ~/Downloads, offer two options:

Option A: Quick-import (recommended)

Run the quick-import helper that opens LinkedIn Analytics in the browser and watches for the download:

node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/quick-import.mjs

This will:

  1. Open linkedin.com/analytics/creator/content/ in your browser
  2. Watch ~/Downloads for new CSV files
  3. Auto-copy detected files to the exports directory

After the script completes, continue to Step 4.

Option B: Manual export

  1. Go to linkedin.com/analytics/creator/content/
  2. Click the "Export" button (top right)
  3. LinkedIn will download a CSV file
  4. Move it to: ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/exports/
mv ~/Downloads/linkedin_analytics_export*.csv ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/exports/

Once done, run /linkedin:import again.

Step 3: Select Files to Import

If CSV files exist in the exports directory:

  1. List the files with details (name, size, date)
  2. Ask the user which file to import using AskUserQuestion:

Options:

  • Latest — Import the most recent file only
  • All — Import all CSV files
  • Select — Choose a specific file
  • Cancel — Exit import

Step 4: Run Import

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

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

Once the user selects, run the import CLI:

"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" import <filename>

If importing multiple files, run the command for each file sequentially.

Step 5: Capture and Present Results

The CLI prints (see cli.ts handleImport):

  • Posts imported: — count of valid rows (rows with an empty title or an unparseable date are skipped, each with a Warning: line)
  • Date range: — earliest to latest post in the batch
  • Batch ID: and Saved to: posts/<file> — the batch file written
  • Saves entered: — only when the CSV carried a Saves column (manual entry)
  • Reach entered: — only when the CSV carried an Out-of-network (or In-network) column; reports the impressions-weighted share and how many posts carried a reading, so partial coverage is visible instead of reading as a whole-batch number
  • An anomaly block — either Immediate alerts detected: with 🔴/⚠️/ spike/drop lines, or No anomalies detected in imported data.

Surface the CLI's output to the user — for example:

Import successful!
─────────────────────────────────────
Posts imported:  42
Date range:      2025-12-01 to 2026-01-29
Saved to:        posts/2025-12-01-batch-a1b2c3d4.json
Saves entered:   1,204 across 18 post(s) (manual)
Reach entered:   31.4% out-of-network across 12 post(s) (manual, impressions-weighted)

Immediate alerts detected:
─────────────────────────────────────
 [INFO] Post "AI agents are eating..." has unusually high impressions: 21,400 (2.4 std deviations above mean)

The saved file is named posts/YYYY-MM-DD-<shortid>.json (the batch's earliest post date + a short batch id), not by ISO week.

Step 5b: Surface the Anomalies the Importer Detected

The import CLI runs intra-batch anomaly detection during Step 4 (detectAlerts in cli.ts): for the just-imported batch it flags any post whose impressions deviate sharply — by standard deviation — from that batch's own mean, printing either Immediate alerts detected: (🔴/⚠️/ spike/drop lines) or No anomalies detected in imported data.

Surface those lines as-is, and state the scope honestly: a flagged post stands out among the posts in this export, not against a stored historical baseline — the importer keeps no baseline file. Cross-week comparison is Step 6's job.

Present as:

### Import Summary — <batch date range>

X posts imported (Y skipped: empty title or unparseable date)

#### Standout in this batch
 "[hook text...]" — 21,400 impressions (2.4 std dev above this batch's mean)
⚠️ "[hook text...]" — 180 impressions (2.1 std dev below this batch's mean)

(or: "No standout deviations within this batch.")

Cross-week analysis — best day of week, format performance, week-over-week trend, rolling averages — is not computed here; it is produced by /linkedin:report (Step 6), which reads the full post history via the trends/heatmap CLI. Defer that analysis to Step 6 rather than restating it.

Step 6: Analytics-to-Strategy Feedback Loop

After successful import, the analysis fan-out (pillar performance, format performance, day-of-week heatmap, actionable recommendations) is delegated to /linkedin:report — both commands consume the same trends CLI from scripts/analytics/, and keeping a second analysis pipeline here drifted out of sync with report.md.

Step 6a: Run the report

Invoke the report generator and surface its output inline:

Run /linkedin:report (period: 4w)

/linkedin:report will:

  1. Read expertise_areas from ~/.claude/linkedin-studio.local.md
  2. Call trends for impressions and engagement_rate over the last 4 weeks:
    "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period 4w --metric impressions
    "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period 4w --metric engagement_rate
    
  3. Produce the Content Pillar Performance, Format Performance, and Day-of-Week Performance tables, plus exactly 3 actionable recommendations
  4. Return its summary back to this import flow

If /linkedin:report is unavailable (analytics dir empty, tsx missing), fall back to a one-line status: "Import complete — run /linkedin:report manually when analytics are ready."

Step 6b: Update State with Import Date

After successful import and analysis, update the state file:

Read ~/.claude/linkedin-studio.local.md
Set last_import_date to today (YYYY-MM-DD)
Set last_import_week to current ISO week (YYYY-WXX)
Write the updated state file

Step 7: Next Steps

Present next steps using AskUserQuestion based on the analysis results:

If the report's trend is down (impressions or engagement trending DOWN):

  • "Run /linkedin:report for full weekly breakdown"
  • "Run content audit to review strategy"
  • "Analyze your top post to understand what worked"

If the report's trend is up (impressions or engagement trending UP):

  • "Run /linkedin:report for the full numbers"
  • "Create more content in your top format"
  • "Draft your next post while insights are fresh"

If first import:

  • "Run /linkedin:report for your first performance report"
  • "Import 2-3 more weeks for trend analysis"
  • "Tip: Export weekly every Monday for best tracking"

If mixed results:

  • "Run /linkedin:report for complete breakdown"
  • "Review trend analysis for diverging metrics"
  • "Check which formats and topics drove results"

Present using AskUserQuestion with the top 3 most relevant suggestions.

Step 8: Demographics Sync Suggestion

After completing the import workflow, check if ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/audience-insights/demographics.md still has placeholder data:

grep -c '\[Industry name\]\|\[Function\]\|\[Country\]\|\[X\]%' ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/audience-insights/demographics.md 2>/dev/null

If placeholder count is > 10 (still mostly unfilled), suggest:

"While you're in LinkedIn Analytics exporting CSV data, you can also capture your audience demographics. Run /linkedin:setup and choose option 5 (Demographics) to fill in your audience insights with real data."

Error Handling

If the import fails:

  1. Check the CSV format - LinkedIn sometimes changes export format
  2. Verify the file path - Ensure the file is in ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/exports/
  3. Check file permissions - The CLI needs read access
  4. Show the error message and suggest solutions

Common errors:

  • File not found: Check the filename (case-sensitive)
  • Invalid CSV format: Verify this is a LinkedIn analytics export
  • Permission denied: Check file permissions with ls -l

Reference Files

The import system creates:

  • ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/YYYY-MM-DD-<shortid>.json - one JSON batch per import (earliest post date + short batch id)

Weekly and monthly report files (under weekly-reports/ and monthly-reports/) are created separately by /linkedin:report, not by import.

State Tracking

After import:

  • A new batch file posts/YYYY-MM-DD-<shortid>.json is written, one per import — existing batch files are never overwritten; loadAllPosts deduplicates by post id at read time (latest import wins)
  • Intra-batch spike/drop alerts are computed and surfaced (std deviation from the batch's own mean — no persisted baseline)
  • last_import_date and last_import_week are updated in the state file (~/.claude/linkedin-studio.local.md, see Step 6b)