Push Obsidian/local Markdown finished drafts directly to the X Articles drafts, completing the publish family's X long-form output (publish-x-thread produces thread text, this skill produces rich-text long-form drafts).
Client-readable description
What this skill can do
| Client wants | Skill will do | Client can see |
|---|---|---|
| Post a Markdown long-form article to X Articles | Parse title/cover/body images → create a draft in the browser → paste rich text → insert images in place → mechanical validation | Real-time browser operation process, draft URL, validation checklist, and final receipt |
| The article has no cover image | Stop and remind (an X Article without a cover looks bad), only continue without a cover after explicit consent | Reminders and confirmation questions |
| Missing dependency/not logged in/no Premium+ | Stop and clearly point out what is missing | Login/sign-up guidance, do not fill in any credentials on the user's behalf |
Security bottom line: only save the draft,never click "Publish"; the login session stays only in the browser, do not export cookies, do not write any credentials to disk.
How the client uses it
- Say "post <file> as an X Article"; make sure the browser is logged in to X and the account has Premium+ (Articles permission).
- The Agent first runs a dry-run parse and reports: title, cover, number of body images, missing image list; if the cover is missing, ask you first.
- The browser phase is fully visible; after completion give the draft URL, and you manually review and publish it.
Dependencies and installation
npx skills add soia-team/soia-open-skills -g -s soia-pkm-publish-x-article -y
- macOS + Python 3.9+ (pure stdlib; clipboard via osascript, image downsampling via the built-in sips)
- Choose one for the browser side:claude-in-chrome(recommended, reuses your real Chrome's X login session) or the built-in Browser pane (you need to log in to X in it the first time)
- The X account needs Premium+(Articles is a paid feature)
- No private config.yml: this skill does not touch any key/cookie
Logs and completion receipt
After execution it must output: what was done, draft URL, validation results (item by item), skipped/failed items, next step (manual review and publish).
Positioning and loop closure
compose-article-draft produces finished text → this skill pushes X Articles drafts (equivalent to publish-wechat-draft for WeChat Official Accounts). "Only create drafts, never publish" is the same security philosophy as on the WeChat Official Accounts side.
Triggers
| User says | Action |
|---|---|
| "post to X Article" "upload X article" "push to X drafts" | Full workflow (parse → upload → validate) |
| "First see whether this article can be posted to X" | Only run the parse dry-run, report without touching the browser |
Workflow
Step 1: Parse (without touching the browser)
python3 scripts/parse_x_article.py <article.md> > /tmp/x_article.json
python3 scripts/parse_x_article.py <article.md> --html-only > /tmp/x_article_body.html
JSON fields:title / cover_image(only when the article starts with an image) / content_images[](contains block_index and after_text)/ dividers[] / html / missing_images。
Two gates, pass them before touching the browser:
missing_images > 0→ stop, give the user the list of missing images (you can use--search-dirto specify the directory to search for them).cover_imageis null → stop, remind them "X Article looks bad without a cover, suggest adding an image at the top"; only continue if the user explicitly says not to add one (in that case treat all images as body images).
Step 2: Enter the editor
- Browser choice: prefer claude-in-chrome (real Chrome already logged in); otherwise the Browser pane (if not logged in, have the user log in,do not enter credentials on their behalf)。
- Navigate to
https://x.com/compose/articles——what loads is thedrafts listpage, not the editor; directly click the "create/write" button to enter the editor (do not wait for "Add title" to appear, it only renders after the click). - Redirect to login page = not logged in; no Articles entry on the page = no Premium+. In both cases stop and tell the user.
Step 3: Cover + title
- Has cover: click the cover upload control and use file upload to upload
cover_image; X will pop up a media editing layer, **you must click "Apply"** to close it, otherwise the editor is blocked by the overlay and the cover won't take effect. - Title box (placeholder "Add title") enter
title。
Step 4: Paste rich text into the body
python3 scripts/clipboard_x.py html --file /tmp/x_article_body.html
Click the editor body area → Cmd+V. Headings/bold/links/lists/quotes are preserved by the HTML clipboard flavor. After pasting, spot-check whether the text of the first paragraph and the last paragraph are both present.
Step 5: Insert images and dividers (all by block_index from largest to smallestreverse order)
For each content_images[i](reverse order):
python3 scripts/clipboard_x.py image <path> --max-bytes 3000000
- Use
after_textto find the target paragraph in the editor, click it. - Press the End keyto push the cursor to the end of the paragraph (to avoid clicking into a link within the paragraph).
Cmd+Vpaste → wait for "Uploading media/Uploading" to disappear before inserting the next one.
Divider (dividers[], also reverse order): X ignores <hr>, you must click the corresponding position and then use the editor's "Insert > Divider" menu.
Reason for reverse order: the editor is a dynamic document, inserting earlier ones first will move all subsequent anchors.
Step 6: Mechanical verification (not complete unless all pass)
| Check item | Criterion |
|---|---|
| Title | Editor title == JSON title |
| Body first/last sentence | both can be found in the editor |
| Media count | Number of images in the editor == cover(0/1) + content_images count |
| Divider count | == dividers count |
| Draft status | "Saved" autosave marker appears; record the draft URL |
Step 7: Receipt
Draft URL + item-by-item results of the verification checklist + "Please review manually and click publish yourself".Under no circumstances click publish.
Format support boundaries
| Element | Handling |
|---|---|
| H2/H3, bold, italic, links, ordered/unordered lists, quotes | Preserved natively |
| Code block | Convert to a quote block (X does not support <pre>) |
| Tables / mermaid | X does not support it: first manually convert to a PNG image and insert it, or accept the degraded loss; the parser passes the text through as-is |
--- Divider |
Menu insertion (see step 5) |
| H1 | Only as a heading, not in the body |
Boundaries and exceptions
| Scenario | Handling |
|---|---|
| Redirect to login page | Stop, have the user log in in the browser and retry; do not touch credentials |
| No Articles entry | Stop, indicate X Premium+ is required |
| Missing image | Step 1 gate intercepts, provide a checklist |
| No cover | Gate reminder, continue only after explicit consent |
| Formatting lost after pasting | Check the clipboard script output; retry once; if it still fails, report the actual result, do not falsely claim success |
| Image upload gets stuck | Wait for the upload indicator to disappear; if it times out, take a screenshot and report, do not blindly continue |
| Controls cannot be found after an X editor redesign | Stop and report the actual page state, do not guess at clicks |
Script specifications (scripts/)
| Script | Responsibility | Dependencies |
|---|---|---|
parse_x_article.py |
MD → JSON (title/cover/image/divider/HTML); adapted from wshuyi/x-article-publisher-skill (MIT, see THIRD_PARTY_NOTICES in the repository) | stdlib |
clipboard_x.py |
HTML/image to clipboard (osascript;--max-bytes automatically downsample to a temporary copy when over the limit) |
stdlib + macOS osascript/sips |
Verification and testing
Script-layer fixture forward test (no X account needed, must run after changing scripts):
- Create a test article containing frontmatter title, a cover image at the top of the article, 2 body images (png+jpg),
---divider, code blocks, ordered/unordered lists, and links. parse_x_article.pyAssertions:titletaken from frontmatter;cover_imageis an absolute path and points to the top-of-article image;content_images[].block_indexincrements,after_texthas no___CODE___marker and has had markdown notation stripped; after deleting the top-of-article image,cover_imageis null (gate semantics).clipboard_x.py htmlafter,osascript -e 'clipboard info'must contain«class HTML»;imageproduce for png/jpg respectively«class PNGf»/JPEG picture;--max-byteswhen over the limit, use a sips temporary copy andthe original file's byte count remains unchanged。
Browser end-to-end (requires a real X Premium+ login session): run the full process with the above test article, with all green on the step 6 verification checklist + a draft URL as the pass criteria; when control changes are caused by an X editor redesign, update this document's selector descriptions based on the actual page state.
Completion receipt
- What was done: one sentence + draft URL.
- Verification checklist: item-by-item results for step 6; mark any failed items truthfully.
- Next step: publish on your own after manual review; recommended to use
soia-pkm-cover-imageto generate the cover.