linkedin-post
/linkedin-post
Pobiera post lub artykuł z LinkedIn: autora, nagłówek, pełny tekst, metryki zaangażowania, obrazy i udostępniane adresy URL, używając przeglądarki gstack, i zapisuje je w katalogu.
- Autor
- mayank-io ↗
- Repozytorium
- mstack ↗
- Licencja
- MIT
- Ostatnia zmiana repozytorium
- 12.10.2026
Ocena i bezpieczeństwo
- Pliki w paczce
- 2
- Skrypty
- 1
- Komentarze HTML
- 0
- Adresy URL w SKILL.md
- 3
Brak sygnałów. Repozytorium zmienione 12.10.2026, bez odwołań do wycofanych modeli i API.
MIT, otwarta: można używać i zmieniać, z zachowaniem informacji o autorze.
- 1 skrypt w paczce. Przejrzyj je przed instalacją, bo Claude może je uruchomić.
Skrypty: references/article_extraction.js
Co robi i kiedy
Pobiera post lub artykuł z LinkedIn: autora, nagłówek, pełny tekst, metryki zaangażowania, obrazy i udostępniane adresy URL, używając przeglądarki gstack, i zapisuje je w katalogu.
Wyzwalacze z opisu
Oryginalny opis (EN)
Extract a LinkedIn post or article — author, headline, full text, engagement metrics, images, and any URLs it shares — using the gstack browser. Use when the user says \"read this linkedin post\", \"get this linkedin post\", \"extract this linkedin post\", \"get this linkedin article\", or shares a linkedin.com/posts, linkedin.com/feed/update or linkedin.com/pulse URL. Returns content and images to a directory; it does not write notes.
Jak działa
- Route first: article or post
- Browser — always gstack, never headless
- Input
- Step 1 — Navigate and expand
- Step 2 — Extract
- Step 3 — Download the attachments
- Step 4 — Report
- Article mode — linkedin.com/pulse/…
- ⚠️ LinkedIn serves a truncated public view on post permalinks
- Failure modes worth naming
Podgląd SKILL.md
---
name: linkedin-post
description: "Extract a LinkedIn post or article — author, headline, full text, engagement metrics, images, and any URLs it shares — using the gstack browser. Use when the user says \"read this linkedin post\", \"get this linkedin post\", \"extract this linkedin post\", \"get this linkedin article\", or shares a linkedin.com/posts, linkedin.com/feed/update or linkedin.com/pulse URL. Returns content and images to a directory; it does not write notes."
---
# Fetch LinkedIn Post
Pull a LinkedIn post's or article's content and media into a directory. **This skill knows nothing about vaults** — it retrieves, and stops. `notes:clip` turns the result into a note.
## Route first: article or post
| URL | Mode | Run |
|---|---|---|
| `linkedin.com/pulse/<slug>` | **Article** | [Article mode](#article-mode--linkedincompulse) — one script, then stop. Skip Steps 1–4. |
| `linkedin.com/posts/…`, `linkedin.com/feed/update/…` | **Post** | Steps 1–4 below |
**Never run post-mode steps on an article.** Each one fails silently there: the "Feed post" scoping heading does not exist, there is no "…more" expander, the slug carries no activity id to decode a date from (`-6vsuc` is not one), and the post-mode image filter rejects `article-cover_image` and does not know `article-inline_image`, so every image in the body is dropped.
## Browser — always gstack, never headless
LinkedIn shows almost nothing to a logged-out session, and what it does show looks like a valid short post rather than an error. Browser work goes through the **gstack browser**, which holds the user's session.
```bash
B="$HOME/.claude/skills/gstack/browse/dist/browse"
"$B" connect # run from the target directory — another cwd spawns a second
# daemon and kills the headed session
"$B" goto "<url>"
"$B" js '<expression>'
```
**The daemon must be in `headed` mode.** `browse status` reports either `headed`
(attached to the user's real Chrome, carrying their logins) or `launched` (gstack's
own Chromium on a fresh profile, logged into nothing). A `launched` daemon returns
a login wall for every gated page, and a login wall reads as a *short page* rather
than an error — nothing downstream will flag it. Verify the mode, and force a
restart when it is wrong:
```bash
"$B" status # must report `mode: headed`
"$B" connect --force-restart # only when it does not — a launched daemon holds a
# fresh profile with no logins, so nothing is lost
```
The `_browse.py` adapter runs this check inside `connect()` and refuses to continue
if it cannot reach `headed`. Do the same by hand when driving `$B` directly.
**Run `$B` outside the command sandbox.** Claude Code's Bash sandbox blocks the connection to the daemon, and `$B status` then reports `Headed server running (PID …) but not responding` — which reads as a hung daemon but is not one. Verified 2026-09-30: the same PID reported `healthy, headed` when the identical command ran unsandboxed, before and after. **Before force-restarting a "not responding" daemon, re-run `status` unsandboxed.**
**Do NOT `disconnect` when done.** `browse disconnect` tears down the daemon and
the logged-in sessions with it. Leave it running — the daemon is a shared user
resource, `connect` is safe to call again, and only whoever started it should
close it. The adapter's `close()` is deliberately a no-op, so leaving the
`async with browse_page()` block tears down nothing.
**Never launch a headless browser.** Not `headless=True`, not `--headless`, not a fresh `chromium.launch()`. If gstack is unavailable, stop and say so — a logged-out capture is worse than none, because it looks fine.
**Page JavaScript must be synchronous, and values are passed as arguments.** `$B js`
returns before a promise resolves, so `evaluate()` refuses any expression that is an
`async` function or contains `await` — the result would be silently lost. Drive the
waiting and looping from Python with `await page.wait_for_timeout(ms)` between
synchronous `evaluate()` calls. Pass values with `await page.evaluate(js, arg)`
rather than string-interpolating them into the JavaScript: an interpolated value
containing a quote breaks the expression.
## Input
`$ARGUMENTS`:
- **First argument** — LinkedIn URL (required): a post (`linkedin.com/posts/…`, `linkedin.com/feed/update/…`) or an article (`linkedin.com/pulse/…`)
- **Second argument** — output directory (optional). Omitted, a temp directory is used. Created if missing. **Never write outside it.**
## Step 1 — Navigate and expand
```bash
"$B" goto "<url>"
```
### Expanding the body
**LinkedIn truncates long posts and hides the rest behind a "…more" control.** A truncated capture reads as a complete short post, so expanding is not optional.
The control is **not always present**, and where it appears depends on the surface:
| Surface | "…more" present? |
|---|---|
| A company or member **posts listing** — `linkedin.com/company/<name>/posts/` | **Yes**, on every post whose text overflows |
| A **post permalink** — `linkedin.com/posts/<slug>-<id>` | **Sometimes.** The permalink can render the truncated public variant with no expander at all (2026-08-24), or the full signed-in view with a working "… more" (2026-09-30: 5 clicks over 2 rounds, full 30-item body). **Always run the expand loop first**; fall back only when it clicks nothing. |
**So try the listing surface when the permalink is short.** If a permalink yields a body under ~250 characters with no expander, the same post on its author's `/posts/` listing usually carries the full text plus a working "…more".
### Match the control by its TEXT, never by class or aria-label
**LinkedIn ships obfuscated, rotating class names.** The real control looks like this:
```
<button class="_5a3a1b6d _22e49b38 _060cf413 _6093ac50 …">… more</button>
```
Hashed classes, and **`aria-label` is empty**. Every class-based or aria-based selector fails silently — it matches nothing and reports zero expanders, which is indistinguishable from a post that needed no expansion. The only stable signal is the **button's text: `… more`**.
Match it exactly, and **require either the ellipsis or the verb**. Making both optional is the trap: `/^(…|\.\.\.)?\s*(see|show)?\s*more$/i` collapses to `/^more$/i`, which matches LinkedIn's **global footer "More" dropdown** on every page. Anchoring alone is not enough — it kills `"More actions for <company>"` but not bare `"More"`.
```javascript
() => {
const clicked = [];
Array.from(document.querySelectorAll('button')).forEach(b => {
const t = (b.innerText || '').trim();
// ellipsis form ("…more") OR verb form ("see more"/"show more") — never bare "More"
if (!/^(?:…|\.\.\.)\s*more$|^(?:see|show)\s+more$/i.test(t)) return;
const inComment = !!b.closest('[class*="comments-comment"]'); // comments, not body
if (inComment) return;
b.click();
clicked.push(t);
});
return clicked; // WHAT was clicked, not just how many
}
```
**Return the clicked labels, not a count.** A bare `n` is an unfalsifiable success signal: a loop clicking the wrong control forever looks identical to one doing real work. `["More"]` in the result is visibly wrong on sight; `1` is not.
### Loop until no expanders remain — one pass is not enough
Expanding reveals **more posts**, each with its own control. Measured on a real company listing: round 1 clicked 3, which surfaced 10 more; round 2 clicked those 10; round 3 found none.
```python
rounds = []
for _ in range(8):
clicked = await page.evaluate(EXPAND_JS)
rounds.append(clicked)
if not clicked:
break # converged
await page.wait_for_timeout(1800)
```
**Read `rounds` before trusting the result.** The same label repeating every round with no character growth means you are re-clicking one control that does nothing — site chrome, not the body.
**Verified 2026-08-24** on `linkedin.com/company/kodiakai/posts/`: 13 expanders over 3 rounds, body **5,725 → 11,628 characters — 2× the text**. Stopping after one pass would have captured roughly half the page and looked complete.
**Report the character delta.** A click that adds ~50 characters expanded a comment, not the post — the signature of hitting the wrong control, which otherwise looks like success.
### Login walls
If the page shows a login form, retry once; the daemon carries existing cookies. If it persists, **stop and ask the user to log in inside the gstack browser**, then retry. Do not fall back to a logged-out fetch.
## Step 2 — Extract
Use `"$B" snapshot` for the accessibility tree, and `$B js` for anything structural. Collect:
| Field | Notes |
|---|---|
| `author_name` | display name |
| `author_headline` | title / company line beneath the name |
| `date` | LinkedIn shows a relative age ("2w") — **resolve it from the activity ID, not the label** (see below) |
| `text` | **the expanded text** — verify "…see more" is gone |
| `images` | post images, at the largest available resolution |
| `metrics` | likes, comments, reposts |
| `links` | every URL in the post body **and** in any link-preview card |
### Resolving the date
**Do not report LinkedIn's relative label as the post date.** Decode the **activity ID** in the URL — `linkedin.com/posts/<slug>-<id>-<code>` or `/feed/update/urn:li:activity:<id>`:
```python
from datetime import datetime, timezone
datetime.fromtimestamp((int(activity_id) >> 22) / 1000, timezone.utc) # no epoch offset
```
Unlike Twitter's snowflake this needs **no epoch offset** — `id >> 22` is epoch milliseconds directly. Corroborate against the upload timestamp embedded in the attachment's CDN path (`/feedshare-shrink_800/<blob>/0/<epoch_ms>?…`); the two should agree to within seconds.
**The relative label can reflect an edit, not the post.** Verified 2026-09-13: a post displaying `1d • Edited` decoded to 2026-09-11 16:00:49 UTC — two days old. Report the decoded date and note the edit separately.
**Return `links` — do not follow them.** Recursing into shared content is `notes:clip`'s job; a fetch skill that pulls in a YouTube video has stopped being a fetch skill. Capture the preview card's title, description and image too: it is often the only trace left when the target link rots.
**Clean `links` before returning them — a raw anchor harvest is mostly noise.** Three traps, all seen on one post (2026-09-30):
- **Every external link is wrapped** as `linkedin.com/safety/go/?url=<encoded target>&urlhash=…`. Unwrap to the `url` parameter; the wrapper is not the link.
- **LinkedIn auto-links bare filenames as domains.** A post that says "CLAUDE.md" and "SKILL.md" produced `safety/go/?url=http://CLAUDE.md` and `http://SKILL.md`. Drop any target whose "host" ends in a file extension (`.md`, `.json`, `.py`, `.txt`, …) — there is nothing there to follow.
- **Scope to the post card, and drop the author's "Visit my website" button.** `document.querySelectorAll('a')` also collects comment links, navigation and the footer. Class selectors (`.feed-shared-update-v2`, `[class*="comments-"]`) match **nothing** on the obfuscated DOM and silently fall back to the whole page. The stable anchor is the visually hidden `<h2>Feed post</h2>` heading: its `[role="listitem"]` ancestor (`componentkey="update-card-focus…"`) holds the header, body, attachment and metrics — **and not the comments**, which sit in a sibling container.
```javascript
() => {
const h = [...document.querySelectorAll('h2')].find(e => (e.innerText || '').trim() === 'Feed post');
const post = (h && h.closest('[role="listitem"]')) || document.querySelector('[componentkey^="update-card-"]');
if (!post) return {error: 'post card not found'}; // never fall back to document.body
const out = new Set();
post.querySelectorAll('a[href]').forEach(a => {
if (/^visit my website$/i.test((a.innerText || '').trim())) return; // author header button
let u = a.href;
try { const p = new URL(u); if (p.pathname.startsWith('/safety/go')) u = p.searchParams.get('url') || ''; } catch { return; }
if (!u || /linkedin\.com\//.test(u)) return; // profiles, hashtags, self
try { if (/\.(md|json|ya?ml|py|js|ts|txt|sh)$/i.test(new URL(u).hostname)) return; } catch { return; }
out.add(u);
});
return {links: [...out]};
}
```
**Verified 2026-09-30** on a post that shares nothing: the class-selector version missed the card and returned 17 URLs (nav, premium upsell, footer, author website, two example URLs from a comment). This version returns `[]` — and with the "Visit my website" line removed it returns the unwrapped `https://skool.com/tec/about`, which proves the unwrap path works and the empty result is not a dead selector. The card's text was 2,197 characters against 7,950 for card + comments.
Check the result against the body text: every link returned should correspond to something the post visibly shares. An empty result is correct for a post that shares nothing.
⚠️ Page JavaScript stays synchronous — see the browser section above. `fetch:blog-post` Step 3 shows the Python-driven loop shape.
## Step 3 — Download the attachments
**The attachment is frequently the content.** A post whose body reads "The Ultimate List of AI Neolabs — there are now 63 of them!" carries those 63 names in an attached image, not in text. Capturing the body and skipping the image captures the caption and loses the post.
### Tell attachments apart from page furniture
Filtering by size alone does not work — avatars are 400×400 and pass any sensible threshold. Discriminate on the **URL path segment**, which LinkedIn sets by role.
Measured 2026-08-24 against the live site:
| Surface | naive `naturalWidth >= 200` | path-segment filter |
|---|---|---|
| A post permalink | 2 images | **1** — the actual attachment |
| A company posts listing | 5 images | **1** — the actual attachment |
| (an earlier read of the same permalink) | 8 images | **1** — the other 7 were avatars, a banner and sidebar covers |
| URL contains | What it is | Take it? |
|---|---|---|
| `/image-shrink_` , `/feedshare-shrink_` , `/feedshare-image-high-res` , `/feedshare-` | **post attachment** — all four observed live | ✅ yes |
| `/profile-displayphoto` | author or commenter avatar | ❌ no |
| `/profile-displaybackgroundimage` | profile banner | ❌ no |
| `/article-cover_image` | "more from LinkedIn" sidebar | ❌ no |
| `/company-logo_` , `/spotlight-` | brand furniture | ❌ no |
```javascript
() => Array.from(document.querySelectorAll('img'))
.map(i => i.currentSrc || i.src || '')
.filter(u => /\/(image-shrink_|feedshare-shrink_|feedshare-)/.test(u))
.filter((u, n, a) => a.indexOf(u) === n)
```
### Ask for the largest rendition
LinkedIn encodes the rendition in the path — `image-shrink_800` is a downscale. Same reasoning as requesting `name=orig` on X: **a downscaled image is unreadable exactly when it matters** — a list, a table, a chart, a screenshot of text.
**Read `srcset` first — rewriting a signed URL does not work.** `feedshare-shrink_*` URLs carry `e=` (expiry) and `t=` (signature) bound to the exact path, so changing the size invalidates the signature:
```javascript
() => Array.from(document.querySelectorAll('img'))
.filter(i => /feedshare|image-shrink_/.test(i.currentSrc || i.src || ''))
.map(i => ({src: i.currentSrc || i.src, srcset: i.srcset || '', w: i.naturalWidth, h: i.naturalHeight}))
```
Take the largest candidate `srcset` offers. **On a width tie, prefer `feedshare-image-high-res`** — verified 2026-09-30, `srcset` listed both `feedshare-image-high-res` and `feedshare-shrink_1280` as `1024w`; the high-res variant downloaded at 1024×1280. Only if there is none, try the path rewrite — it may still work on unsigned `image-shrink_` URLs — and fall back:
```bash
big="${url/image-shrink_800/image-shrink_1280}"
curl -fsL -o "<output_dir>/linkedin-<author>-<n>.jpg" "$big" \
|| curl -fsL -o "<output_dir>/linkedin-<author>-<n>.jpg" "$url"
```
**Report the pixel dimensions you actually got.** Verified 2026-09-13: on a signed `feedshare-shrink_800` URL with no `srcset`, both `_1280` and `_2048` failed and 800×1000 was the ceiling. That is a fine outcome — silently presenting it as full resolution is not. Say the cap was hit so a reader knows the transcription came from an upscale.
### Carousels and documents
A post can attach a multi-page PDF carousel rather than a single image. Those paginate — capture **every** page, and report the page count. One page of a twelve-page carousel is not the attachment.
**Detect with `.native-document, [class*="document-s-container"]`, corroborated by an `N / M` page counter in the page text.**
⚠️ **Never use `[class*=carousel]`.** It matches `feed-shared-update-v2--with-carousel-fix` — a CSS bugfix class LinkedIn puts on *every* update, carousel or not. Verified 2026-09-13: it reported a carousel on an ordinary single-image post. A false positive here sends you hunting for pages that do not exist, or worse, reporting "1 of N captured" for a post with one image.
### When curl is refused
LinkedIn's CDN sometimes rejects a bare `curl`. Screenshot the image element through gstack rather than skipping it — **and say in your report that it was screenshotted rather than downloaded**, since a re-encode is not the original asset.
### Read what you downloaded
If the body is short and an attachment is present, **the attachment is the post.** Open the image and describe or transcribe its content into the note. Filing an unexamined image and summarising from the caption is how the list of 63 gets lost.
## Step 4 — Report
Write the extracted fields as JSON alongside the images, then print the directory as the **final stdout line**:
```
OUTPUT_DIR:/absolute/path/to/output
```
The marker must be last. Callers chain on it and must never reconstruct the path.
## Article mode — `linkedin.com/pulse/…`
**This skill owns no article extraction code.** The DOM logic is [`references/article_extraction.js`](references/article_extraction.js), tested under jsdom against a saved copy of a live article; navigation, waiting, validation and downloading are `${CLAUDE_PLUGIN_ROOT}/scripts/linkedin_article_download.py`. Do not re-implement either by hand — run the script:
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/linkedin_article_download.py "<pulse-url>" [out_dir]
```
Run it via the Bash tool **outside the sandbox** (it drives `$B`) with `timeout: 300000`. It goes through `_browse.sync_browse_page()`, so the headed-mode check is automatic.
What it writes into `out_dir` (a temp directory when omitted):
| File | Contents |
|---|---|
| `article.json` | `type: article`, `source`, `title`, `author_name`, `author_headline`, `author_url`, `published_label`, `published` (ISO, or null), `earliest_image_upload_utc`, `date_warning`, `metrics {reactions, comments, reposts}`, `links`, `body_chars`, ordered `blocks`, `images [{url, role, file}]`, `images_failed` |
| `article.md` | plain Markdown of the article in document order, no vault syntax |
| `linkedin-<slug>-cover.jpg`, `linkedin-<slug>-<n>.jpg` | the cover and every inline image, at the widest `srcset` rendition |
**Output contract:** final stdout line `OUTPUT_DIR:<abs path>`. On failure — a non-article URL, no `<article>`, no title, or a body under 300 characters (a login wall or the wrong page) — it exits non-zero with **no** marker. Warnings go to stderr: a printed date more than 2 days from the earliest image upload, or an image that failed to download.
How each article difference is handled, so it is not rediscovered:
| Difference from a post | Handled in |
|---|---|
| Body is **ordered blocks** (paragraphs, `##`/`###` headings, lists, quotes, code, images *between* paragraphs), not one string | `article_extraction.js blocksOf()` — the note must keep image positions |
| Bold run-ins put the separating space **inside** `<strong>`, so naive wrapping fuses words (`**workflow:**Doubleclick`) | `inline()` moves edge whitespace outside the markers |
| **No activity id** — the date is the printed `<time>` label, corroborated by the `/0/<epoch_ms>` upload stamp in the image URLs | `parse_published_label()`, `date_check()`; an unparseable label is `null`, never a guess |
| Cover (`article-cover_image`) and inline (`article-inline_image`) images are **content** | `image_role()`; the cover is returned separately, never as a body block |
| Inline images are **lazy** | the script scrolls to the end and polls `imagesPending()` before extracting |
| Reactions render as a bare number; the unit is only in the **`aria-label`** ("37 reactions") | `metricsOf()` reads aria-labels in `.social-details-social-counts` |
| External links are `safety/go` wrapped | `unwrapLink()`, scoped to the article body |
Verified 2026-10-02 on `linkedin.com/pulse/defensibility-ai-data-lessons-from-ads-gokul-rajaram-6vsuc`: 17 blocks, 6,116 body characters word-for-word identical to a by-hand capture, cover 778×437 and inline image 972×734 between the closing paragraph and the PS, 37 reactions / 4 comments, printed date agreeing with the image upload at 2026-10-02 02:00 UTC.
**Comments are not captured** in article mode; quote any you need from the page by hand.
## ⚠️ LinkedIn serves a truncated public view on post permalinks
**Verified 2026-08-24, and it is not a session problem you can fix by logging in.** With a live LinkedIn session in the gstack browser — `/feed/` rendering fully, notifications present — a `/posts/…` permalink still returned the logged-out chrome ("Sign in · Join now") and a post body of **203 characters**, cut mid-thought at "There are now 63 of them!". The list of 63 was not on the page at all.
What was checked, so you do not repeat it:
| Attempt | Result |
|---|---|
| Session live? | Yes — `/feed/` signed in, notifications visible |
| Auth modal blocking? | No — `.authwall` absent |
| A "Show more" button? | Present, but it expands **comments**. Clicking added 48 characters, all of them replies. |
| A separate expander for the body? | **None exists.** The only expanders are "Show more" and "See more comments". |
| `/feed/update/urn:li:activity:<id>/` (the authenticated form) | Redirects to `/signup/cold-join` |
**Two things to try before accepting the truncation**, in order:
1. **The author's posts listing.** `linkedin.com/company/<name>/posts/` (or a member's `/recent-activity/all/`) renders the same post with a working "…more" control that the permalink lacks. This is the highest-value fallback.
2. **The attachment.** On the post above, the missing list of 63 was **in an attached image all along** — an `800×1491` graphic. The body was genuinely truncated *and* the content was still fully present, just not as text. Always run Step 3 before concluding anything is missing.
Only after both: **capture what is served and say plainly that the body is truncated.** Report the character count and the last words captured. Never present a 203-character fragment as the post.
What comes through reliably regardless: author name, relative date, reaction and comment counts, comment text, attachments, and the link-preview card.
If the full body text is still needed after all that, the reliable route is a human opening the post in their own session and copying it — not more automation.
## Failure modes worth naming
- **A short post that should be long** — either "…see more" was never expanded, or you have hit the permalink truncation above. Distinguish them: check whether a body expander exists at all before blaming the click.
- **Author headline missing** — common on reshares; report it absent rather than substituting the reshared author's.
- **Zero images on a post that clearly has them** — usually lazy-loading. Scroll the post into view, wait, re-collect.
- **A login wall captured as content** — the failure this skill exists to prevent. If the text mentions signing in or joining LinkedIn, treat the capture as failed.
Pliki w paczce (2)
- SKILL.md 24 KB
- references/article_extraction.js 8 KB
Wersje
| Repozytorium | Wynik | Licencja |
|---|---|---|
| Ta strona · mayank-io/mstack | 100 | MIT |
| mickpletcher/AI-Skills | 100 | MIT |
| mickpletcher/AI-Skills | 100 | MIT |
| mickpletcher/AI-Skills | 100 | MIT |
| publora/skills | 100 | MIT |
| ARCASSystems/FounderOS | 100 | MIT |
| langchain-ai/langgraph-101 | 100 | MIT |
| qa-aman/claude-skills | 100 | MIT |
| furkankoykiran/.claude | 100 | MIT |
Migawka bazy z 28.10.2026 · wersja paczki 7f4f96068053 (skrót SHA-256 całej paczki)
Pobrania w ostatnich 30 dniach: 0 · ranking
Zgłoś problem z tym skillem: szkodliwa treść, błędny opis albo prośba autora o usunięcie.
Podobne skille
- 100x-post
Pobiera treść posta, wątku lub artykułu z X do notatki Markdown, z obrazami w oryginalnej rozdzielczości. - 100linkedin-post-research
Przeszukuje posty LinkedIn według słów kluczowych, sortując według zaangażowania lub daty, i zwraca autora, treść, reakcje, komentarze, udostępnienia, adres i datę, bez logowania. - 100content
Zamienia dowolną treść (PDF, mail, newsletter, URL, wklejony tekst, raport) w 3 do 4 gotowych do publikacji postów na LinkedIn. - 85linkedin-posts
Pomaga pisać i optymalizować posty na LinkedIn: artykuły, treści B2B, posty z dokumentem, ankiety, newslettery i reshare'y pod zaangażowanie. - 100linkedin-content
Tworzy skuteczne posty LinkedIn i strategię treści: hooki, posty eksperckie i profesjonalne. - 95write-post
Pisze post na LinkedIn przez serwer LinkedIn Writer MCP, także z grafiką, na podstawie badań i wytycznych.