Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions skills/typefully/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ The format is based on Keep a Changelog.

### Added

- `drafts:create` and `drafts:update` now support LinkedIn reshares/reposts with `--linkedin-reshare-target <url_or_urn>`.
- LinkedIn reshare aliases: `--linkedin_reshare_target`, `--linkedin-reshare-url`, `--linkedin-repost-url`, `--reshare-url`, and `--repost-url`.
- Skill docs now explain LinkedIn reshare/repost workflows, including plain reshares without commentary.
- `analytics:posts:list` now supports `--include-replies` (alias: `--include_replies`) to opt in to X reply posts.

### Changed
Expand Down
41 changes: 40 additions & 1 deletion skills/typefully/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: >
Create, schedule, and manage social media posts via Typefully. ALWAYS use this
skill when asked to draft, schedule, post, or check tweets, posts, threads, or
social media content for Twitter/X, LinkedIn, Threads, Bluesky, or Mastodon.
last-updated: 2026-03-17
last-updated: 2026-04-28
allowed-tools: Bash(./scripts/typefully.js:*)
---

Expand Down Expand Up @@ -110,6 +110,7 @@ When determining which social set to use:
| "Draft a tweet about X" | `drafts:create --text "..."` (uses default social set) |
| "Post this to LinkedIn" | `drafts:create --platform linkedin --text "..."` |
| "Mention a company on LinkedIn" | `linkedin:organizations:resolve --organization-url "<linkedin_url>"` then use returned `mention_text` in `drafts:create` |
| "Reshare/repost this LinkedIn post" | `drafts:create --platform linkedin --linkedin-reshare-target "<linkedin_url_or_urn>"` |
| "Post to X and LinkedIn" (same content) | `drafts:create --platform x,linkedin --text "..."` |
| "X thread + LinkedIn post" (different content) | Create one draft, then `drafts:update` to add platform (see [Publishing to Multiple Platforms](#publishing-to-multiple-platforms)) |
| "What's scheduled?" | `drafts:list --status scheduled` |
Expand Down Expand Up @@ -225,6 +226,27 @@ Then include that `mention_text` in your LinkedIn draft text:
./scripts/typefully.js drafts:create --platform linkedin --text "Thanks @[Typefully](urn:li:organization:86779668) for the support."
```

## LinkedIn Reshares/Reposts

LinkedIn reshares are supported by adding a LinkedIn post URL or canonical parent URN to the LinkedIn post payload:

```bash
# Reshare/repost a LinkedIn post with commentary
./scripts/typefully.js drafts:create --platform linkedin --text "Worth reading" --linkedin-reshare-target "https://www.linkedin.com/posts/typefullycom_linkedin-mentions-just-got-a-lot-better-in-activity-7437089188157554688-wSxN"

# Plain reshare/repost with no commentary
./scripts/typefully.js drafts:create --platform linkedin --linkedin-reshare-target "urn:li:share:7437089188157554688"
```

The API also accepts `urn:li:ugcPost:<id>` and `urn:li:groupPost:<id>` values. The CLI sends this as `linkedin_reshare_target`; draft responses return the canonical `linkedin_reshare_urn`.

Useful aliases:
- `--linkedin_reshare_target`
- `--linkedin-reshare-url`
- `--linkedin-repost-url`
- `--reshare-url`
- `--repost-url`

## Commands Reference

### User & Social Sets
Expand Down Expand Up @@ -274,10 +296,12 @@ All drafts commands support an optional `[social_set_id]` - if omitted, the conf
| `drafts:create ... --reply-to <url>` | Reply to an existing X post |
| `drafts:create ... --community <id>` | Post to an X community |
| `drafts:create ... --quote-post-url <url>` | Quote an existing X post URL |
| `drafts:create ... --linkedin-reshare-target <url_or_urn>` | Reshare/repost an existing LinkedIn post |
| `drafts:create ... --share` | Generate a public share URL for the draft |
| `drafts:create ... --scratchpad "..."` | Add internal notes/scratchpad to the draft |
| `drafts:update [social_set_id] <draft_id> --text "..."` | Update an existing draft (single-arg requires `--use-default` if a default is configured) |
| `drafts:update ... --quote-post-url <url>` | Update X post(s) in a draft to quote an existing post URL |
| `drafts:update ... --linkedin-reshare-target <url_or_urn>` | Update LinkedIn post(s) in a draft to reshare/repost an existing LinkedIn post |
| `drafts:update [social_set_id] <draft_id> --tags "tag1,tag2"` | Update tags on an existing draft (content unchanged) |
| `drafts:update ... --share` | Generate a public share URL for the draft |
| `drafts:update ... --scratchpad "..."` | Update internal notes/scratchpad |
Expand Down Expand Up @@ -379,6 +403,21 @@ Use `queue:get` when the user asks what is already scheduled (or free) for a giv
./scripts/typefully.js drafts:create --platform linkedin --text "Thanks @[Typefully](urn:li:organization:86779668) for the support."
```

### Create a LinkedIn reshare/repost
```bash
./scripts/typefully.js drafts:create --platform linkedin --text "Worth reading" --linkedin-reshare-target "https://www.linkedin.com/posts/typefullycom_linkedin-mentions-just-got-a-lot-better-in-activity-7437089188157554688-wSxN"
```

### Create a plain LinkedIn reshare/repost
```bash
./scripts/typefully.js drafts:create --platform linkedin --linkedin-reshare-target "urn:li:share:7437089188157554688"
```

### Update a LinkedIn draft to reshare/repost a post
```bash
./scripts/typefully.js drafts:update 456 --platform linkedin --linkedin-reshare-target "urn:li:ugcPost:7346543409643380737" --use-default
```

### Create a post on all connected platforms
```bash
./scripts/typefully.js drafts:create --all --text "Posting everywhere!"
Expand Down
133 changes: 118 additions & 15 deletions skills/typefully/scripts/typefully.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,53 @@ function addQuotePostUrl(posts, quotePostUrl) {
return posts.map(post => ({ ...post, quote_post_url: quotePostUrl }));
}

function getLinkedInReshareTargetFromParsed(parsed) {
const keys = [
'linkedin-reshare-target',
'linkedin_reshare_target',
'linkedin-reshare-url',
'linkedin_reshare_url',
'linkedin-repost-url',
'linkedin_repost_url',
'reshare-url',
'reshare_url',
'repost-url',
'repost_url',
];
const values = [];

for (const key of keys) {
if (!Object.prototype.hasOwnProperty.call(parsed, key)) continue;
values.push({
key,
value: coerceFlagValueToString(parsed[key], `--${key}`),
});
}

if (values.length === 0) return null;

const first = values[0].value;
const conflicting = values.find(item => item.value !== first);
if (conflicting) {
error('Conflicting LinkedIn reshare target values', Object.fromEntries(values.map(item => [`--${item.key}`, item.value])));
}

return first;
}

function addLinkedInReshareTarget(posts, linkedinReshareTarget) {
if (!linkedinReshareTarget) return posts;
return posts.map(post => ({ ...post, linkedin_reshare_target: linkedinReshareTarget }));
}

function splitDraftText(text, { allowEmpty = false } = {}) {
const posts = splitThreadText(text || '');
if (posts.length === 0 && allowEmpty) {
return [''];
}
return posts;
}

function parseCsvArg(value, flagName) {
// parseArgs sets missing values to true (e.g. `--tags --other-flag`)
if (value === true) {
Expand Down Expand Up @@ -992,6 +1039,7 @@ async function cmdDraftsCreate(args) {
const parsed = parseArgs(args, { share: 'boolean', all: 'boolean' });
const socialSetId = resolveSocialSetIdFromParsed(parsed, parsed._positional[0]);
const quotePostUrl = getQuotePostUrlFromParsed(parsed);
const linkedinReshareTarget = getLinkedInReshareTargetFromParsed(parsed);

// Get text content
let text = parsed.text;
Expand All @@ -1002,8 +1050,8 @@ async function cmdDraftsCreate(args) {
text = fs.readFileSync(parsed.file, 'utf-8');
}

if (!text) {
error('--text or --file is required');
if (!text && !linkedinReshareTarget) {
error('--text, --file, or --linkedin-reshare-target is required');
}

// Determine platform(s)
Expand All @@ -1020,6 +1068,8 @@ async function cmdDraftsCreate(args) {
error('No connected platforms found. Connect a platform at typefully.com');
}
platforms = allPlatforms.join(',');
} else if (!platforms && linkedinReshareTarget) {
platforms = 'linkedin';
} else if (!platforms) {
// Smart default: get first connected platform
const defaultPlatform = await getFirstConnectedPlatform(socialSetId);
Expand All @@ -1033,9 +1083,15 @@ async function cmdDraftsCreate(args) {
if (quotePostUrl && !platformList.includes('x')) {
error('--quote-post-url is only supported for X posts. Include x in --platform or remove the quote flag.');
}
if (linkedinReshareTarget && !platformList.includes('linkedin')) {
error('--linkedin-reshare-target is only supported for LinkedIn posts. Include linkedin in --platform or remove the reshare flag.');
}
if (linkedinReshareTarget && !text && platformList.some(platform => platform !== 'linkedin')) {
error('A text value is required when creating non-LinkedIn posts alongside a LinkedIn reshare.');
}

// Split text into posts (thread support)
const posts = splitThreadText(text);
const posts = splitDraftText(text, { allowEmpty: Boolean(linkedinReshareTarget) });

// Parse media IDs
const mediaIds = parsed.media ? parsed.media.split(',').map(m => m.trim()) : [];
Expand All @@ -1053,9 +1109,13 @@ async function cmdDraftsCreate(args) {
// Build platforms object
const platformsObj = {};
for (const platform of platformList) {
const postsArray = platform === 'x'
? addQuotePostUrl(basePostsArray, quotePostUrl)
: basePostsArray;
let postsArray = basePostsArray;
if (platform === 'x') {
postsArray = addQuotePostUrl(postsArray, quotePostUrl);
}
if (platform === 'linkedin') {
postsArray = addLinkedInReshareTarget(postsArray, linkedinReshareTarget);
}
const platformConfig = {
enabled: true,
posts: postsArray,
Expand Down Expand Up @@ -1106,6 +1166,7 @@ async function cmdDraftsUpdate(args) {
const parsed = parseArgs(args, { append: 'boolean', share: 'boolean', 'use-default': 'boolean' });
const { socialSetId, draftId } = resolveDraftTargetFromParsed(parsed, 'drafts:update');
const quotePostUrl = getQuotePostUrlFromParsed(parsed);
const linkedinReshareTarget = getLinkedInReshareTargetFromParsed(parsed);

// Get text content
let text = parsed.text;
Expand All @@ -1118,14 +1179,17 @@ async function cmdDraftsUpdate(args) {

const body = {};

const shouldUpdatePosts = Boolean(text || quotePostUrl);
const shouldUpdatePosts = Boolean(text || quotePostUrl || linkedinReshareTarget);
if (shouldUpdatePosts) {
const explicitPlatformList = parsed.platform
? parsed.platform.split(',').map(p => p.trim())
: null;
if (quotePostUrl && explicitPlatformList && !explicitPlatformList.includes('x')) {
error('--quote-post-url is only supported for X posts. Include x in --platform or remove the quote flag.');
}
if (linkedinReshareTarget && explicitPlatformList && !explicitPlatformList.includes('linkedin')) {
error('--linkedin-reshare-target is only supported for LinkedIn posts. Include linkedin in --platform or remove the reshare flag.');
}

// Parse media IDs
const mediaIds = parsed.media ? parsed.media.split(',').map(m => m.trim()) : [];
Expand Down Expand Up @@ -1157,6 +1221,9 @@ async function cmdDraftsUpdate(args) {
if (quotePostUrl && !platformList.includes('x')) {
error('--quote-post-url is only supported for X posts. Include x in --platform or remove the quote flag.');
}
if (linkedinReshareTarget && !platformList.includes('linkedin')) {
error('--linkedin-reshare-target is only supported for LinkedIn posts. Include linkedin in --platform or remove the reshare flag.');
}

let postsArray;

Expand All @@ -1179,7 +1246,7 @@ async function cmdDraftsUpdate(args) {
postsArray = [...existingPosts, newPost];
} else {
// Replace with new posts
const posts = splitThreadText(text);
const posts = splitDraftText(text);
postsArray = posts.map((postText, index) => {
const post = { text: postText };
if (index === 0 && mediaIds.length > 0) {
Expand All @@ -1188,22 +1255,35 @@ async function cmdDraftsUpdate(args) {
return post;
});
}
} else {
} else if (quotePostUrl) {
// Quote-only update: preserve existing X posts and add quote URL.
const existingXPosts = existing.platforms?.x?.posts;
if (!Array.isArray(existingXPosts) || existingXPosts.length === 0) {
error('Cannot apply --quote-post-url because this draft has no existing X posts');
}
postsArray = existingXPosts;
platformList = ['x'];
} else {
// Reshare-only update: preserve existing LinkedIn posts and add the reshare target.
const existingLinkedInPosts = existing.platforms?.linkedin?.posts;
if (!Array.isArray(existingLinkedInPosts) || existingLinkedInPosts.length === 0) {
postsArray = [{ text: '' }];
} else {
postsArray = existingLinkedInPosts;
}
platformList = ['linkedin'];
}

// Build platforms object
const platformsObj = {};
for (const p of platformList) {
const platformPosts = p === 'x'
? addQuotePostUrl(postsArray, quotePostUrl)
: postsArray;
let platformPosts = postsArray;
if (p === 'x') {
platformPosts = addQuotePostUrl(platformPosts, quotePostUrl);
}
if (p === 'linkedin') {
platformPosts = addLinkedInReshareTarget(platformPosts, linkedinReshareTarget);
}
platformsObj[p] = {
enabled: true,
posts: platformPosts,
Expand Down Expand Up @@ -1233,7 +1313,7 @@ async function cmdDraftsUpdate(args) {
}

if (Object.keys(body).length === 0) {
error('At least one of --text, --file, --title, --schedule, --share, --notes, --tags, or --quote-post-url is required');
error('At least one of --text, --file, --title, --schedule, --share, --notes, --tags, --quote-post-url, or --linkedin-reshare-target is required');
}

const data = await apiRequest('PATCH', `/social-sets/${socialSetId}/drafts/${draftId}`, body);
Expand All @@ -1247,6 +1327,7 @@ async function cmdDraftsUpdate(args) {
async function cmdCreateDraftAlias(args) {
const parsed = parseArgs(args, { share: 'boolean', all: 'boolean' });
const socialSetId = requireSocialSetId(getSocialSetIdFromParsed(parsed));
const linkedinReshareTarget = getLinkedInReshareTargetFromParsed(parsed);

const forwarded = [String(socialSetId)];

Expand All @@ -1258,12 +1339,14 @@ async function cmdCreateDraftAlias(args) {
if (Object.prototype.hasOwnProperty.call(parsed, 'text')) {
text = coerceFlagValueToString(parsed.text, '--text');
} else {
if (parsed._positional.length === 0) {
if (parsed._positional.length === 0 && !linkedinReshareTarget) {
error('Draft text is required (provide it as the first argument, or use --text/--file)');
}
text = parsed._positional.join(' ');
}
forwarded.push('--text', text);
if (text || !linkedinReshareTarget) {
forwarded.push('--text', text);
}
}

pushStringFlag(forwarded, parsed, 'platform', '--platform');
Expand All @@ -1276,6 +1359,7 @@ async function cmdCreateDraftAlias(args) {
pushStringFlag(forwarded, parsed, 'community', '--community');
const quotePostUrl = getQuotePostUrlFromParsed(parsed);
if (quotePostUrl) forwarded.push('--quote-post-url', quotePostUrl);
if (linkedinReshareTarget) forwarded.push('--linkedin-reshare-target', linkedinReshareTarget);
if (parsed.share) forwarded.push('--share');
pushStringFlag(forwarded, parsed, 'notes', '--notes');

Expand Down Expand Up @@ -1313,6 +1397,8 @@ async function cmdUpdateDraftAlias(args) {
pushStringFlag(forwarded, parsed, 'tags', '--tags', { allowEmpty: true });
const quotePostUrl = getQuotePostUrlFromParsed(parsed);
if (quotePostUrl) forwarded.push('--quote-post-url', quotePostUrl);
const linkedinReshareTarget = getLinkedInReshareTargetFromParsed(parsed);
if (linkedinReshareTarget) forwarded.push('--linkedin-reshare-target', linkedinReshareTarget);
if (parsed.share) forwarded.push('--share');
pushStringFlag(forwarded, parsed, 'notes', '--notes');

Expand Down Expand Up @@ -1609,6 +1695,10 @@ COMMANDS:
--reply-to <url> URL of X post to reply to
--community <id> X community ID to post to
--quote-post-url, --quote-url <url> Quote an X post URL (X only)
--linkedin-reshare-target <url_or_urn> Reshare/repost a LinkedIn post (LinkedIn only)
Also accepts: --linkedin_reshare_target,
--linkedin-reshare-url, --linkedin-repost-url,
--reshare-url, --repost-url
--share Generate a public share URL for the draft
--notes, --scratchpad <text> Internal notes/scratchpad for the draft

Expand All @@ -1623,6 +1713,10 @@ COMMANDS:
--schedule <time> "now", "next-free-slot", or ISO datetime
--tags <tag_slugs> Comma-separated tag slugs
--quote-post-url, --quote-url <url> Quote an X post URL (X only)
--linkedin-reshare-target <url_or_urn> Reshare/repost a LinkedIn post (LinkedIn only)
Also accepts: --linkedin_reshare_target,
--linkedin-reshare-url, --linkedin-repost-url,
--reshare-url, --repost-url
--share Generate a public share URL for the draft
--notes, --scratchpad <text> Internal notes/scratchpad for the draft
--use-default Required when using default social set with single arg
Expand Down Expand Up @@ -1762,6 +1856,15 @@ EXAMPLES:
# Update an X draft to quote a post
./typefully.js drafts:update 123 456 --platform x --text "Updated take" --quote-post-url "https://x.com/user/status/1234567890123456789"

# Create a LinkedIn reshare/repost with commentary
./typefully.js drafts:create 123 --platform linkedin --text "Worth reading" --linkedin-reshare-target "https://www.linkedin.com/posts/typefullycom_linkedin-mentions-just-got-a-lot-better-in-activity-7437089188157554688-wSxN"

# Create a plain LinkedIn reshare/repost with no commentary
./typefully.js drafts:create 123 --platform linkedin --linkedin-reshare-target "urn:li:share:7437089188157554688"

# Update a LinkedIn draft to reshare/repost an existing post
./typefully.js drafts:update 123 456 --platform linkedin --linkedin-reshare-target "urn:li:ugcPost:7346543409643380737"

# Create draft with share URL
./typefully.js drafts:create 123 --platform x --text "Check this out" --share

Expand Down
Loading