Compare commits

...

2 Commits

Author SHA1 Message Date
Jim Liu 宝玉 e3d2f5d03f Add clipboard paste functionality and documentation for X Articles and regular posts
- Implemented `paste-from-clipboard.ts` script to send real paste keystrokes across macOS, Linux, and Windows.
- Created detailed guides for publishing articles to X Articles and posting regular content, including usage examples and troubleshooting tips.
- Added support for Markdown formatting and image handling in X Articles.
- Documented manual workflows for image pasting and browser interactions using Playwright MCP.
2026-01-15 15:38:03 -06:00
Jim Liu 宝玉 da920bb830 feat: add baoyu- prefix 2026-01-15 14:18:56 -06:00
59 changed files with 983 additions and 474 deletions
+8 -8
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "0.3.1"
"version": "0.4.1"
},
"plugins": [
{
@@ -15,13 +15,13 @@
"source": "./",
"strict": false,
"skills": [
"./skills/gemini-web",
"./skills/xhs-images",
"./skills/post-to-x",
"./skills/post-to-wechat",
"./skills/article-illustrator",
"./skills/cover-image",
"./skills/slide-deck"
"./skills/baoyu-gemini-web",
"./skills/baoyu-xhs-images",
"./skills/baoyu-post-to-x",
"./skills/baoyu-post-to-wechat",
"./skills/baoyu-article-illustrator",
"./skills/baoyu-cover-image",
"./skills/baoyu-slide-deck"
]
}
]
+20 -15
View File
@@ -10,12 +10,13 @@ Claude Code marketplace plugin providing AI-powered content generation skills. S
```
skills/
├── gemini-web/ # Core: Gemini API wrapper (text + image gen)
├── xhs-images/ # Xiaohongshu infographic series (1-10 images)
├── cover-image/ # Article cover images (2.35:1 aspect)
├── slide-deck/ # Presentation slides with outlines
├── article-illustrator/ # Smart illustration placement
── post-to-x/ # X/Twitter posting automation
├── baoyu-gemini-web/ # Core: Gemini API wrapper (text + image gen)
├── baoyu-xhs-images/ # Xiaohongshu infographic series (1-10 images)
├── baoyu-cover-image/ # Article cover images (2.35:1 aspect)
├── baoyu-slide-deck/ # Presentation slides with outlines
├── baoyu-article-illustrator/ # Smart illustration placement
── baoyu-post-to-x/ # X/Twitter posting automation
└── baoyu-post-to-wechat/ # WeChat Official Account posting
```
Each skill contains:
@@ -34,24 +35,24 @@ npx -y bun skills/<skill>/scripts/main.ts [options]
Examples:
```bash
# Text generation
npx -y bun skills/gemini-web/scripts/main.ts "Hello"
npx -y bun skills/baoyu-gemini-web/scripts/main.ts "Hello"
# Image generation
npx -y bun skills/gemini-web/scripts/main.ts --prompt "A cat" --image cat.png
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --prompt "A cat" --image cat.png
# From prompt files
npx -y bun skills/gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
```
## Key Dependencies
- **Bun**: TypeScript runtime (via `npx -y bun`)
- **Chrome**: Required for `gemini-web` auth and `post-to-x` automation
- **Chrome**: Required for `baoyu-gemini-web` auth and `baoyu-post-to-x` automation
- **No npm packages**: Self-contained TypeScript, no external dependencies
## Authentication
`gemini-web` uses browser cookies for Google auth:
`baoyu-gemini-web` uses browser cookies for Google auth:
- First run opens Chrome for login
- Cookies cached in data directory
- Force refresh: `--login` flag
@@ -62,10 +63,14 @@ npx -y bun skills/gemini-web/scripts/main.ts --promptfiles system.md content.md
## Adding New Skills
1. Create `skills/<name>/SKILL.md` with YAML front matter
2. Add TypeScript in `skills/<name>/scripts/`
3. Add prompt templates in `skills/<name>/prompts/` if needed
4. Register in `marketplace.json` plugins[0].skills array
**IMPORTANT**: All skills MUST use `baoyu-` prefix to avoid conflicts when users import this plugin.
1. Create `skills/baoyu-<name>/SKILL.md` with YAML front matter
- Directory name: `baoyu-<name>`
- SKILL.md `name` field: `baoyu-<name>`
2. Add TypeScript in `skills/baoyu-<name>/scripts/`
3. Add prompt templates in `skills/baoyu-<name>/prompts/` if needed
4. Register in `marketplace.json` plugins[0].skills array as `./skills/baoyu-<name>`
## Code Style
+19
View File
@@ -34,6 +34,25 @@ Run the following command in Claude Code:
/plugin install content-skills@baoyu-skills
```
**Option 3: Ask the Agent**
Simply tell Claude Code:
> Please install Skills from github.com/JimLiu/baoyu-skills
## Update Skills
To update skills to the latest version:
1. Run `/plugin` in Claude Code
2. Switch to **Marketplaces** tab (use arrow keys or Tab)
3. Select **baoyu-skills**
4. Choose **Update marketplace**
You can also **Enable auto-update** to get the latest versions automatically.
![Update Skills](./screenshots/update-plugins.png)
## Available Skills
### gemini-web
+19
View File
@@ -34,6 +34,25 @@
/plugin install content-skills@baoyu-skills
```
**方式三:告诉 Agent**
直接告诉 Claude Code
> 请帮我安装 github.com/JimLiu/baoyu-skills 中的 Skills
## 更新技能
更新技能到最新版本:
1. 在 Claude Code 中运行 `/plugin`
2. 切换到 **Marketplaces** 标签页(使用方向键或 Tab
3. 选择 **baoyu-skills**
4. 选择 **Update marketplace**
也可以选择 **Enable auto-update** 启用自动更新,每次启动时自动获取最新版本。
![更新技能](./screenshots/update-plugins.png)
## 可用技能
### gemini-web
Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

@@ -1,5 +1,5 @@
---
name: article-illustrator
name: baoyu-article-illustrator
description: Smart article illustration skill. Analyzes article content and generates illustrations at positions requiring visual aids with multiple style options. Use when user asks to "add illustrations to article", "generate images for article", or "illustrate article".
---
@@ -11,15 +11,15 @@ Analyze article structure and content, identify positions requiring visual aids,
```bash
# Auto-select style based on content
/article-illustrator path/to/article.md
/baoyu-article-illustrator path/to/article.md
# Specify a style
/article-illustrator path/to/article.md --style tech
/article-illustrator path/to/article.md --style warm
/article-illustrator path/to/article.md --style minimal
/baoyu-article-illustrator path/to/article.md --style tech
/baoyu-article-illustrator path/to/article.md --style warm
/baoyu-article-illustrator path/to/article.md --style minimal
# Combine with other options
/article-illustrator path/to/article.md --style playful
/baoyu-article-illustrator path/to/article.md --style playful
```
## Options
@@ -1,5 +1,5 @@
---
name: cover-image
name: baoyu-cover-image
description: Generate elegant cover images for articles. Analyzes content and creates eye-catching hand-drawn style cover images with multiple style options. Use when user asks to "generate cover image", "create article cover", or "make a cover for article".
---
@@ -11,25 +11,25 @@ Generate hand-drawn style cover images for articles with multiple style options.
```bash
# From markdown file (auto-select style based on content)
/cover-image path/to/article.md
/baoyu-cover-image path/to/article.md
# Specify a style
/cover-image path/to/article.md --style tech
/cover-image path/to/article.md --style warm
/cover-image path/to/article.md --style bold
/baoyu-cover-image path/to/article.md --style tech
/baoyu-cover-image path/to/article.md --style warm
/baoyu-cover-image path/to/article.md --style bold
# Without title text
/cover-image path/to/article.md --no-title
/baoyu-cover-image path/to/article.md --no-title
# Combine options
/cover-image path/to/article.md --style minimal --no-title
/baoyu-cover-image path/to/article.md --style minimal --no-title
# From direct text input
/cover-image
/baoyu-cover-image
[paste content or describe the topic]
# Direct input with style
/cover-image --style playful
/baoyu-cover-image --style playful
[paste content]
```
@@ -1,5 +1,5 @@
---
name: gemini-web
name: baoyu-gemini-web
description: Interacts with Gemini Web to generate text and images. Use when the user needs AI-generated content via Gemini, including text responses and image generation.
---
@@ -100,7 +100,7 @@ export async function loadGeminiCookies(
}
log?.(
'[gemini-web] Missing Gemini auth cookies. Run `npx -y bun skills/gemini-web/scripts/main.ts --login` to sign in and refresh cookies.',
'[gemini-web] Missing Gemini auth cookies. Run `npx -y bun skills/baoyu-gemini-web/scripts/main.ts --login` to sign in and refresh cookies.',
);
return merged;
}
@@ -124,7 +124,7 @@ export function createGeminiWebExecutor(
const cookieMap = await loadGeminiCookies(runOptions.config, log);
if (!hasRequiredGeminiCookies(cookieMap)) {
throw new Error(
'Gemini browser mode requires auth cookies (missing __Secure-1PSID/__Secure-1PSIDTS). Run `npx -y bun skills/gemini-web/scripts/main.ts --login` to sign in and save cookies.',
'Gemini browser mode requires auth cookies (missing __Secure-1PSID/__Secure-1PSIDTS). Run `npx -y bun skills/baoyu-gemini-web/scripts/main.ts --login` to sign in and save cookies.',
);
}
@@ -16,10 +16,10 @@ function printUsage(exitCode = 0): never {
const profileDir = resolveGeminiWebChromeProfileDir();
console.log(`Usage:
npx -y bun skills/gemini-web/scripts/main.ts --prompt "Hello"
npx -y bun skills/gemini-web/scripts/main.ts "Hello"
npx -y bun skills/gemini-web/scripts/main.ts --prompt "A cute cat" --image generated.png
npx -y bun skills/gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --prompt "Hello"
npx -y bun skills/baoyu-gemini-web/scripts/main.ts "Hello"
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --prompt "A cute cat" --image generated.png
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
Options:
-p, --prompt <text> Prompt text
@@ -1,5 +1,5 @@
---
name: post-to-wechat
name: baoyu-post-to-wechat
description: Post content to WeChat Official Account (微信公众号). Supports both article posting (文章) and image-text posting (图文).
---
@@ -7,25 +7,45 @@ description: Post content to WeChat Official Account (微信公众号). Supports
Post content to WeChat Official Account using Chrome CDP automation.
## Script Directory
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
**Agent Execution Instructions**:
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
3. Replace all `${SKILL_DIR}` in this document with the actual path
**Script Reference**:
| Script | Purpose |
|--------|---------|
| `scripts/wechat-browser.ts` | Image-text posts (图文) |
| `scripts/wechat-article.ts` | Full article posting (文章) |
| `scripts/md-to-wechat.ts` | Markdown → WeChat HTML conversion |
| `scripts/copy-to-clipboard.ts` | Copy content to clipboard |
| `scripts/paste-from-clipboard.ts` | Send real paste keystroke |
## Quick Usage
### Image-Text (图文) - Multiple images with title/content
```bash
# From markdown file and image directory
npx -y bun ./scripts/wechat-browser.ts --markdown article.md --images ./images/
npx -y bun ${SKILL_DIR}/scripts/wechat-browser.ts --markdown article.md --images ./images/
# With explicit parameters
npx -y bun ./scripts/wechat-browser.ts --title "标题" --content "内容" --image img1.png --image img2.png --submit
npx -y bun ${SKILL_DIR}/scripts/wechat-browser.ts --title "标题" --content "内容" --image img1.png --image img2.png --submit
```
### Article (文章) - Full markdown with formatting
```bash
# Post markdown article
npx -y bun ./scripts/wechat-article.ts --markdown article.md --theme grace
npx -y bun ${SKILL_DIR}/scripts/wechat-article.ts --markdown article.md --theme grace
```
> **Note**: `${SKILL_DIR}` represents this skill's installation directory. Agent replaces with actual path at runtime.
## References
- **Image-Text Posting**: See `references/image-text-posting.md` for detailed image-text posting guide
@@ -0,0 +1,194 @@
import { spawnSync } from 'node:child_process';
import process from 'node:process';
function printUsage(exitCode = 0): never {
console.log(`Send real paste keystroke (Cmd+V / Ctrl+V) to the frontmost application
This bypasses CDP's synthetic events which websites can detect and ignore.
Usage:
npx -y bun paste-from-clipboard.ts [options]
Options:
--retries <n> Number of retry attempts (default: 3)
--delay <ms> Delay between retries in ms (default: 500)
--app <name> Target application to activate first (macOS only)
--help Show this help
Examples:
# Simple paste
npx -y bun paste-from-clipboard.ts
# Paste to Chrome with retries
npx -y bun paste-from-clipboard.ts --app "Google Chrome" --retries 5
# Quick paste with shorter delay
npx -y bun paste-from-clipboard.ts --delay 200
`);
process.exit(exitCode);
}
function sleepSync(ms: number): void {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
function activateApp(appName: string): boolean {
if (process.platform !== 'darwin') return false;
// Activate and wait for app to be frontmost
const script = `
tell application "${appName}"
activate
delay 0.5
end tell
-- Verify app is frontmost
tell application "System Events"
set frontApp to name of first application process whose frontmost is true
if frontApp is not "${appName}" then
tell application "${appName}" to activate
delay 0.3
end if
end tell
`;
const result = spawnSync('osascript', ['-e', script], { stdio: 'pipe' });
return result.status === 0;
}
function pasteMac(retries: number, delayMs: number, targetApp?: string): boolean {
for (let i = 0; i < retries; i++) {
// Build script that activates app (if specified) and sends keystroke in one atomic operation
const script = targetApp
? `
tell application "${targetApp}"
activate
end tell
delay 0.3
tell application "System Events"
keystroke "v" using command down
end tell
`
: `
tell application "System Events"
keystroke "v" using command down
end tell
`;
const result = spawnSync('osascript', ['-e', script], { stdio: 'pipe' });
if (result.status === 0) {
return true;
}
const stderr = result.stderr?.toString().trim();
if (stderr) {
console.error(`[paste] osascript error: ${stderr}`);
}
if (i < retries - 1) {
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
sleepSync(delayMs);
}
}
return false;
}
function pasteLinux(retries: number, delayMs: number): boolean {
// Try xdotool first (X11), then ydotool (Wayland)
const tools = [
{ cmd: 'xdotool', args: ['key', 'ctrl+v'] },
{ cmd: 'ydotool', args: ['key', '29:1', '47:1', '47:0', '29:0'] }, // Ctrl down, V down, V up, Ctrl up
];
for (const tool of tools) {
const which = spawnSync('which', [tool.cmd], { stdio: 'pipe' });
if (which.status !== 0) continue;
for (let i = 0; i < retries; i++) {
const result = spawnSync(tool.cmd, tool.args, { stdio: 'pipe' });
if (result.status === 0) {
return true;
}
if (i < retries - 1) {
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
sleepSync(delayMs);
}
}
return false;
}
console.error('[paste] No supported tool found. Install xdotool (X11) or ydotool (Wayland).');
return false;
}
function pasteWindows(retries: number, delayMs: number): boolean {
const ps = `
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("^v")
`;
for (let i = 0; i < retries; i++) {
const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', ps], { stdio: 'pipe' });
if (result.status === 0) {
return true;
}
if (i < retries - 1) {
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
sleepSync(delayMs);
}
}
return false;
}
function paste(retries: number, delayMs: number, targetApp?: string): boolean {
switch (process.platform) {
case 'darwin':
return pasteMac(retries, delayMs, targetApp);
case 'linux':
return pasteLinux(retries, delayMs);
case 'win32':
return pasteWindows(retries, delayMs);
default:
console.error(`[paste] Unsupported platform: ${process.platform}`);
return false;
}
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
let retries = 3;
let delayMs = 500;
let targetApp: string | undefined;
for (let i = 0; i < args.length; i++) {
const arg = args[i] ?? '';
if (arg === '--help' || arg === '-h') {
printUsage(0);
}
if (arg === '--retries' && args[i + 1]) {
retries = parseInt(args[++i]!, 10) || 3;
} else if (arg === '--delay' && args[i + 1]) {
delayMs = parseInt(args[++i]!, 10) || 500;
} else if (arg === '--app' && args[i + 1]) {
targetApp = args[++i];
} else if (arg.startsWith('-')) {
console.error(`Unknown option: ${arg}`);
printUsage(1);
}
}
if (targetApp) {
console.log(`[paste] Target app: ${targetApp}`);
}
console.log(`[paste] Sending paste keystroke (retries=${retries}, delay=${delayMs}ms)...`);
const success = paste(retries, delayMs, targetApp);
if (success) {
console.log('[paste] Paste keystroke sent successfully');
} else {
console.error('[paste] Failed to send paste keystroke');
process.exit(1);
}
}
await main();
+103
View File
@@ -0,0 +1,103 @@
---
name: baoyu-post-to-x
description: Post content and articles to X (Twitter). Supports regular posts with images and X Articles (long-form Markdown). Uses real Chrome with CDP to bypass anti-automation.
---
# Post to X (Twitter)
Post content, images, and long-form articles to X using real Chrome browser (bypasses anti-bot detection).
## Script Directory
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
**Agent Execution Instructions**:
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
3. Replace all `${SKILL_DIR}` in this document with the actual path
**Script Reference**:
| Script | Purpose |
|--------|---------|
| `scripts/x-browser.ts` | Regular posts (text + images) |
| `scripts/x-article.ts` | Long-form article publishing (Markdown) |
| `scripts/md-to-html.ts` | Markdown → HTML conversion |
| `scripts/copy-to-clipboard.ts` | Copy content to clipboard |
| `scripts/paste-from-clipboard.ts` | Send real paste keystroke |
## Prerequisites
- Google Chrome or Chromium installed
- `bun` installed (for running scripts)
- First run: log in to X in the opened browser window
## References
- **Regular Posts**: See `references/regular-posts.md` for manual workflow, troubleshooting, and technical details
- **X Articles**: See `references/articles.md` for long-form article publishing guide
---
## Regular Posts
Text + up to 4 images.
```bash
# Preview mode (doesn't post)
npx -y bun ${SKILL_DIR}/scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
# Actually post
npx -y bun ${SKILL_DIR}/scripts/x-browser.ts "Hello!" --image ./photo.png --submit
```
> **Note**: `${SKILL_DIR}` represents this skill's installation directory. Agent replaces with actual path at runtime.
**Parameters**:
| Parameter | Description |
|-----------|-------------|
| `<text>` | Post content (positional argument) |
| `--image <path>` | Image file path (can be repeated, max 4) |
| `--submit` | Actually post (default: preview only) |
| `--profile <dir>` | Custom Chrome profile directory |
---
## X Articles
Long-form Markdown articles (requires X Premium).
```bash
# Preview mode
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md
# With cover image
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
# Publish
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --submit
```
**Parameters**:
| Parameter | Description |
|-----------|-------------|
| `<markdown>` | Markdown file path (positional argument) |
| `--cover <path>` | Cover image path |
| `--title <text>` | Override article title |
| `--submit` | Actually publish (default: preview only) |
**Frontmatter** (optional):
```yaml
---
title: My Article Title
cover_image: /path/to/cover.jpg
---
```
---
## Notes
- First run requires manual login (session is saved)
- Always preview before using `--submit`
- Browser closes automatically after operation
- Supports macOS, Linux, and Windows
@@ -0,0 +1,176 @@
# X Articles - Detailed Guide
Publish Markdown articles to X Articles editor with rich text formatting and images.
## Prerequisites
- X Premium subscription (required for Articles)
- Google Chrome installed
- `bun` installed
## Usage
```bash
# Publish markdown article (preview mode)
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md
# With custom cover image
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
# Actually publish
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --submit
```
## Markdown Format
```markdown
---
title: My Article Title
cover_image: /path/to/cover.jpg
---
# Title (becomes article title)
Regular paragraph text with **bold** and *italic*.
## Section Header
More content here.
![Image alt text](./image.png)
- List item 1
- List item 2
1. Numbered item
2. Another item
> Blockquote text
[Link text](https://example.com)
\`\`\`
Code blocks become blockquotes (X doesn't support code)
\`\`\`
```
## Frontmatter Fields
| Field | Description |
|-------|-------------|
| `title` | Article title (or uses first H1) |
| `cover_image` | Cover image path or URL |
| `cover` | Alias for cover_image |
| `image` | Alias for cover_image |
## Image Handling
1. **Cover Image**: First image or `cover_image` from frontmatter
2. **Remote Images**: Automatically downloaded to temp directory
3. **Placeholders**: Images in content use `[[IMAGE_PLACEHOLDER_N]]` format
4. **Insertion**: Placeholders are found, selected, and replaced with actual images
## Markdown to HTML Script
Convert markdown and inspect structure:
```bash
# Get JSON with all metadata
npx -y bun ${SKILL_DIR}/scripts/md-to-html.ts article.md
# Output HTML only
npx -y bun ${SKILL_DIR}/scripts/md-to-html.ts article.md --html-only
# Save HTML to file
npx -y bun ${SKILL_DIR}/scripts/md-to-html.ts article.md --save-html /tmp/article.html
```
JSON output:
```json
{
"title": "Article Title",
"coverImage": "/path/to/cover.jpg",
"contentImages": [
{
"placeholder": "[[IMAGE_PLACEHOLDER_1]]",
"localPath": "/tmp/x-article-images/img.png",
"blockIndex": 5
}
],
"html": "<p>Content...</p>",
"totalBlocks": 20
}
```
## Supported Formatting
| Markdown | HTML Output |
|----------|-------------|
| `# H1` | Title only (not in body) |
| `## H2` - `###### H6` | `<h2>` |
| `**bold**` | `<strong>` |
| `*italic*` | `<em>` |
| `[text](url)` | `<a href>` |
| `> quote` | `<blockquote>` |
| `` `code` `` | `<code>` |
| ```` ``` ```` | `<blockquote>` (X limitation) |
| `- item` | `<ul><li>` |
| `1. item` | `<ol><li>` |
| `![](img)` | Image placeholder |
## Workflow
1. **Parse Markdown**: Extract title, cover, content images, generate HTML
2. **Launch Chrome**: Real browser with CDP, persistent login
3. **Navigate**: Open `x.com/compose/articles`
4. **Create Article**: Click create button if on list page
5. **Upload Cover**: Use file input for cover image
6. **Fill Title**: Type title into title field
7. **Paste Content**: Copy HTML to clipboard, paste into editor
8. **Insert Images**: For each placeholder (reverse order):
- Find placeholder text in editor
- Select the placeholder
- Copy image to clipboard
- Paste to replace selection
9. **Review**: Browser stays open for 60s preview
10. **Publish**: Only with `--submit` flag
## Example Session
```
User: /post-to-x article ./blog/my-post.md --cover ./thumbnail.png
Claude:
1. Parses markdown: title="My Post", 3 content images
2. Launches Chrome with CDP
3. Navigates to x.com/compose/articles
4. Clicks create button
5. Uploads thumbnail.png as cover
6. Fills title "My Post"
7. Pastes HTML content
8. Inserts 3 images at placeholder positions
9. Reports: "Article composed. Review and use --submit to publish."
```
## Troubleshooting
- **No create button**: Ensure X Premium subscription is active
- **Cover upload fails**: Check file path and format (PNG, JPEG)
- **Images not inserting**: Verify placeholders exist in pasted content
- **Content not pasting**: Check HTML clipboard: `npx -y bun ${SKILL_DIR}/scripts/copy-to-clipboard.ts html --file /tmp/test.html`
## How It Works
1. `md-to-html.ts` converts Markdown to HTML:
- Extracts frontmatter (title, cover)
- Converts markdown to HTML
- Replaces images with unique placeholders
- Downloads remote images locally
- Returns structured JSON
2. `x-article.ts` publishes via CDP:
- Launches real Chrome (bypasses detection)
- Uses persistent profile (saved login)
- Navigates and fills editor via DOM manipulation
- Pastes HTML from system clipboard
- Finds/selects/replaces each image placeholder
@@ -0,0 +1,100 @@
# Regular Posts - Detailed Guide
Detailed documentation for posting text and images to X.
## Manual Workflow
If you prefer step-by-step control:
### Step 1: Copy Image to Clipboard
```bash
npx -y bun ${SKILL_DIR}/scripts/copy-to-clipboard.ts image /path/to/image.png
```
### Step 2: Paste from Clipboard
```bash
# Simple paste to frontmost app
npx -y bun ${SKILL_DIR}/scripts/paste-from-clipboard.ts
# Paste to Chrome with retries
npx -y bun ${SKILL_DIR}/scripts/paste-from-clipboard.ts --app "Google Chrome" --retries 5
# Quick paste with shorter delay
npx -y bun ${SKILL_DIR}/scripts/paste-from-clipboard.ts --delay 200
```
### Step 3: Use Playwright MCP (if Chrome session available)
```bash
# Navigate
mcp__playwright__browser_navigate url="https://x.com/compose/post"
# Get element refs
mcp__playwright__browser_snapshot
# Type text
mcp__playwright__browser_click element="editor" ref="<ref>"
mcp__playwright__browser_type element="editor" ref="<ref>" text="Your content"
# Paste image (after copying to clipboard)
mcp__playwright__browser_press_key key="Meta+v" # macOS
# or
mcp__playwright__browser_press_key key="Control+v" # Windows/Linux
# Screenshot to verify
mcp__playwright__browser_take_screenshot filename="preview.png"
```
## Image Support
- Formats: PNG, JPEG, GIF, WebP
- Max 4 images per post
- Images copied to system clipboard, then pasted via keyboard shortcut
## Example Session
```
User: /post-to-x "Hello from Claude!" --image ./screenshot.png
Claude:
1. Runs: npx -y bun ${SKILL_DIR}/scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
2. Chrome opens with X compose page
3. Text is typed into editor
4. Image is copied to clipboard and pasted
5. Browser stays open 30s for preview
6. Reports: "Post composed. Use --submit to post."
```
## Troubleshooting
- **Chrome not found**: Set `X_BROWSER_CHROME_PATH` environment variable
- **Not logged in**: First run opens Chrome - log in manually, cookies are saved
- **Image paste fails**:
- Verify clipboard script: `npx -y bun ${SKILL_DIR}/scripts/copy-to-clipboard.ts image <path>`
- On macOS, grant "Accessibility" permission to Terminal/iTerm in System Settings > Privacy & Security > Accessibility
- Keep Chrome window visible and in front during paste operations
- **osascript permission denied**: Grant Terminal accessibility permissions in System Preferences
- **Rate limited**: Wait a few minutes before retrying
## How It Works
The `x-browser.ts` script uses Chrome DevTools Protocol (CDP) to:
1. Launch real Chrome (not Playwright) with `--disable-blink-features=AutomationControlled`
2. Use persistent profile directory for saved login sessions
3. Interact with X via CDP commands (Runtime.evaluate, Input.dispatchKeyEvent)
4. **Paste images using osascript** (macOS): Sends real Cmd+V keystroke to Chrome, bypassing CDP's synthetic events that X can detect
This approach bypasses X's anti-automation detection that blocks Playwright/Puppeteer.
### Image Paste Mechanism (macOS)
CDP's `Input.dispatchKeyEvent` sends "synthetic" keyboard events that websites can detect. X ignores synthetic paste events for security. The solution:
1. Copy image to system clipboard via Swift/AppKit (`copy-to-clipboard.ts`)
2. Bring Chrome to front via `osascript`
3. Send real Cmd+V keystroke via `osascript` and System Events
4. Wait for upload to complete
This requires Terminal to have "Accessibility" permission in System Settings.
@@ -303,6 +303,7 @@ export async function parseMarkdown(
// Resolve image paths (download remote, resolve relative)
const contentImages: ImageInfo[] = [];
let isFirstImage = true;
let coverPlaceholder: string | null = null;
for (let i = 0; i < images.length; i++) {
const img = images[i]!;
@@ -311,6 +312,7 @@ export async function parseMarkdown(
// First image becomes cover if no cover specified
if (isFirstImage && !coverImagePath) {
coverImagePath = localPath;
coverPlaceholder = `[[IMAGE_PLACEHOLDER_${i + 1}]]`;
isFirstImage = false;
// Don't add to contentImages, it's the cover
continue;
@@ -325,6 +327,13 @@ export async function parseMarkdown(
});
}
// Remove cover placeholder from HTML if first image was used as cover
let finalHtml = html;
if (coverPlaceholder) {
// Remove the placeholder and its containing <p> tag
finalHtml = finalHtml.replace(new RegExp(`<p>${coverPlaceholder.replace(/[[\]]/g, '\\$&')}</p>\\n?`, 'g'), '');
}
// Resolve cover image path
let resolvedCoverImage: string | null = null;
if (coverImagePath) {
@@ -335,7 +344,7 @@ export async function parseMarkdown(
title,
coverImage: resolvedCoverImage,
contentImages,
html,
html: finalHtml,
totalBlocks,
};
}
@@ -0,0 +1,194 @@
import { spawnSync } from 'node:child_process';
import process from 'node:process';
function printUsage(exitCode = 0): never {
console.log(`Send real paste keystroke (Cmd+V / Ctrl+V) to the frontmost application
This bypasses CDP's synthetic events which websites can detect and ignore.
Usage:
npx -y bun paste-from-clipboard.ts [options]
Options:
--retries <n> Number of retry attempts (default: 3)
--delay <ms> Delay between retries in ms (default: 500)
--app <name> Target application to activate first (macOS only)
--help Show this help
Examples:
# Simple paste
npx -y bun paste-from-clipboard.ts
# Paste to Chrome with retries
npx -y bun paste-from-clipboard.ts --app "Google Chrome" --retries 5
# Quick paste with shorter delay
npx -y bun paste-from-clipboard.ts --delay 200
`);
process.exit(exitCode);
}
function sleepSync(ms: number): void {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
function activateApp(appName: string): boolean {
if (process.platform !== 'darwin') return false;
// Activate and wait for app to be frontmost
const script = `
tell application "${appName}"
activate
delay 0.5
end tell
-- Verify app is frontmost
tell application "System Events"
set frontApp to name of first application process whose frontmost is true
if frontApp is not "${appName}" then
tell application "${appName}" to activate
delay 0.3
end if
end tell
`;
const result = spawnSync('osascript', ['-e', script], { stdio: 'pipe' });
return result.status === 0;
}
function pasteMac(retries: number, delayMs: number, targetApp?: string): boolean {
for (let i = 0; i < retries; i++) {
// Build script that activates app (if specified) and sends keystroke in one atomic operation
const script = targetApp
? `
tell application "${targetApp}"
activate
end tell
delay 0.3
tell application "System Events"
keystroke "v" using command down
end tell
`
: `
tell application "System Events"
keystroke "v" using command down
end tell
`;
const result = spawnSync('osascript', ['-e', script], { stdio: 'pipe' });
if (result.status === 0) {
return true;
}
const stderr = result.stderr?.toString().trim();
if (stderr) {
console.error(`[paste] osascript error: ${stderr}`);
}
if (i < retries - 1) {
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
sleepSync(delayMs);
}
}
return false;
}
function pasteLinux(retries: number, delayMs: number): boolean {
// Try xdotool first (X11), then ydotool (Wayland)
const tools = [
{ cmd: 'xdotool', args: ['key', 'ctrl+v'] },
{ cmd: 'ydotool', args: ['key', '29:1', '47:1', '47:0', '29:0'] }, // Ctrl down, V down, V up, Ctrl up
];
for (const tool of tools) {
const which = spawnSync('which', [tool.cmd], { stdio: 'pipe' });
if (which.status !== 0) continue;
for (let i = 0; i < retries; i++) {
const result = spawnSync(tool.cmd, tool.args, { stdio: 'pipe' });
if (result.status === 0) {
return true;
}
if (i < retries - 1) {
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
sleepSync(delayMs);
}
}
return false;
}
console.error('[paste] No supported tool found. Install xdotool (X11) or ydotool (Wayland).');
return false;
}
function pasteWindows(retries: number, delayMs: number): boolean {
const ps = `
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("^v")
`;
for (let i = 0; i < retries; i++) {
const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', ps], { stdio: 'pipe' });
if (result.status === 0) {
return true;
}
if (i < retries - 1) {
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
sleepSync(delayMs);
}
}
return false;
}
function paste(retries: number, delayMs: number, targetApp?: string): boolean {
switch (process.platform) {
case 'darwin':
return pasteMac(retries, delayMs, targetApp);
case 'linux':
return pasteLinux(retries, delayMs);
case 'win32':
return pasteWindows(retries, delayMs);
default:
console.error(`[paste] Unsupported platform: ${process.platform}`);
return false;
}
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
let retries = 3;
let delayMs = 500;
let targetApp: string | undefined;
for (let i = 0; i < args.length; i++) {
const arg = args[i] ?? '';
if (arg === '--help' || arg === '-h') {
printUsage(0);
}
if (arg === '--retries' && args[i + 1]) {
retries = parseInt(args[++i]!, 10) || 3;
} else if (arg === '--delay' && args[i + 1]) {
delayMs = parseInt(args[++i]!, 10) || 500;
} else if (arg === '--app' && args[i + 1]) {
targetApp = args[++i];
} else if (arg.startsWith('-')) {
console.error(`Unknown option: ${arg}`);
printUsage(1);
}
}
if (targetApp) {
console.log(`[paste] Target app: ${targetApp}`);
}
console.log(`[paste] Sending paste keystroke (retries=${retries}, delay=${delayMs}ms)...`);
const success = paste(retries, delayMs, targetApp);
if (success) {
console.log('[paste] Paste keystroke sent successfully');
} else {
console.error('[paste] Failed to send paste keystroke');
process.exit(1);
}
}
await main();
@@ -141,44 +141,30 @@ class CdpConnection {
}
}
function getScriptDir(): string {
return path.dirname(new URL(import.meta.url).pathname);
}
function copyImageToClipboard(imagePath: string): boolean {
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
const copyScript = path.join(scriptDir, 'copy-to-clipboard.ts');
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
return result.status === 0;
}
function copyHtmlToClipboard(htmlPath: string): boolean {
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
const copyScript = path.join(scriptDir, 'copy-to-clipboard.ts');
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'html', '--file', htmlPath], { stdio: 'inherit' });
return result.status === 0;
}
function sendRealPasteKeystroke(retries = 3): boolean {
if (process.platform !== 'darwin') {
console.log('[x-article] Real keystroke only supported on macOS');
return false;
function pasteFromClipboard(targetApp?: string, retries = 3, delayMs = 500): boolean {
const pasteScript = path.join(getScriptDir(), 'paste-from-clipboard.ts');
const args = ['npx', '-y', 'bun', pasteScript, '--retries', String(retries), '--delay', String(delayMs)];
if (targetApp) {
args.push('--app', targetApp);
}
// Use osascript to send Cmd+V to frontmost application (Chrome)
const script = `
tell application "System Events"
keystroke "v" using command down
end tell
`;
for (let i = 0; i < retries; i++) {
const result = spawnSync('osascript', ['-e', script], { stdio: 'pipe' });
if (result.status === 0) {
return true;
}
// Wait a bit before retry
if (i < retries - 1) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
}
}
return false;
const result = spawnSync(args[0]!, args.slice(1), { stdio: 'inherit' });
return result.status === 0;
}
interface ArticleOptions {
@@ -300,11 +286,6 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
}, { sessionId });
};
const pasteFromClipboard = async (): Promise<void> => {
const modifiers = process.platform === 'darwin' ? 4 : 2; // Meta or Ctrl
await pressKey('v', modifiers);
};
// Check if we're on the articles list page (has Write button)
console.log('[x-article] Looking for Write button...');
const writeButtonFound = await waitForElement('[data-testid="empty_state_button_text"]', 10_000);
@@ -538,7 +519,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
}, { sessionId });
// Wait for scroll animation
await sleep(300);
await sleep(500);
if (!placeholderFound.result.value) {
console.warn(`[x-article] Placeholder not found in DOM: ${img.placeholder}`);
@@ -553,25 +534,26 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
continue;
}
await sleep(300);
// Wait for clipboard to be fully ready
await sleep(800);
// Delete placeholder by pressing Enter (placeholder is already selected)
console.log(`[x-article] Deleting placeholder with Enter...`);
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, { sessionId });
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, { sessionId });
await sleep(200);
await sleep(300);
// Paste image
// Paste image using paste script (activates Chrome, sends real keystroke)
console.log(`[x-article] Pasting image...`);
if (sendRealPasteKeystroke()) {
if (pasteFromClipboard('Google Chrome', 5, 800)) {
console.log(`[x-article] Image pasted: ${path.basename(img.localPath)}`);
} else {
console.warn(`[x-article] Failed to paste image`);
console.warn(`[x-article] Failed to paste image after retries`);
}
// Wait for image to upload
console.log(`[x-article] Waiting for upload...`);
await sleep(4000);
await sleep(5000);
}
console.log('[x-article] All images processed.');
@@ -633,17 +615,15 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
console.log('[x-article] Article published!');
} else {
console.log('[x-article] Article composed (draft mode).');
console.log('[x-article] Browser will stay open for 60 seconds for review...');
await sleep(60_000);
console.log('[x-article] Browser remains open for manual review.');
}
} finally {
// Disconnect CDP but keep browser open
if (cdp) {
try { await cdp.send('Browser.close', {}, { timeoutMs: 5_000 }); } catch {}
cdp.close();
}
setTimeout(() => { if (!chrome.killed) try { chrome.kill('SIGKILL'); } catch {} }, 2_000).unref?.();
try { chrome.kill('SIGTERM'); } catch {}
// Don't kill Chrome - let user review and close manually
}
}
@@ -8,6 +8,26 @@ import process from 'node:process';
const X_COMPOSE_URL = 'https://x.com/compose/post';
function getScriptDir(): string {
return path.dirname(new URL(import.meta.url).pathname);
}
function copyImageToClipboard(imagePath: string): boolean {
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
return result.status === 0;
}
function pasteFromClipboard(targetApp?: string, retries = 3, delayMs = 500): boolean {
const pasteScript = path.join(getScriptDir(), 'paste-from-clipboard.ts');
const args = ['npx', '-y', 'bun', pasteScript, '--retries', String(retries), '--delay', String(delayMs)];
if (targetApp) {
args.push('--app', targetApp);
}
const result = spawnSync(args[0]!, args.slice(1), { stdio: 'inherit' });
return result.status === 0;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
@@ -274,37 +294,46 @@ export async function postToX(options: XBrowserOptions): Promise<void> {
console.log(`[x-browser] Pasting image: ${imagePath}`);
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
const copyScript = path.join(scriptDir, 'copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
if (result.status !== 0) {
if (!copyImageToClipboard(imagePath)) {
console.warn(`[x-browser] Failed to copy image to clipboard: ${imagePath}`);
continue;
}
// Wait for clipboard to be ready
await sleep(500);
// Focus the editor
await cdp.send('Runtime.evaluate', {
expression: `document.querySelector('[data-testid="tweetTextarea_0"]')?.focus()`,
}, { sessionId });
await sleep(200);
const modifiers = process.platform === 'darwin' ? 4 : 2;
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyDown',
key: 'v',
code: 'KeyV',
modifiers,
windowsVirtualKeyCode: 86,
}, { sessionId });
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyUp',
key: 'v',
code: 'KeyV',
modifiers,
windowsVirtualKeyCode: 86,
}, { sessionId });
// Use paste script (handles platform differences, activates Chrome)
console.log('[x-browser] Pasting from clipboard...');
const pasteSuccess = pasteFromClipboard('Google Chrome', 5, 500);
if (!pasteSuccess) {
// Fallback to CDP (may not work for images on X)
console.log('[x-browser] Paste script failed, trying CDP fallback...');
const modifiers = process.platform === 'darwin' ? 4 : 2;
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyDown',
key: 'v',
code: 'KeyV',
modifiers,
windowsVirtualKeyCode: 86,
}, { sessionId });
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyUp',
key: 'v',
code: 'KeyV',
modifiers,
windowsVirtualKeyCode: 86,
}, { sessionId });
}
console.log('[x-browser] Waiting for image upload...');
await sleep(3000);
await sleep(4000);
}
if (submit) {
@@ -1,5 +1,5 @@
---
name: slide-deck
name: baoyu-slide-deck
description: Generate professional slide deck images from content. Creates comprehensive outlines with style instructions, then generates individual slide images. Use when user asks to "create slides", "make a presentation", "generate deck", or "slide deck".
---
@@ -11,26 +11,26 @@ Transform content into professional slide deck with comprehensive outlines and g
```bash
# From markdown file
/slide-deck path/to/article.md
/baoyu-slide-deck path/to/article.md
# With style preference
/slide-deck path/to/article.md --style corporate
/slide-deck path/to/article.md --style playful
/slide-deck path/to/article.md --style technical
/baoyu-slide-deck path/to/article.md --style corporate
/baoyu-slide-deck path/to/article.md --style playful
/baoyu-slide-deck path/to/article.md --style technical
# With audience specification
/slide-deck path/to/article.md --audience beginners
/slide-deck path/to/article.md --audience executives
/baoyu-slide-deck path/to/article.md --audience beginners
/baoyu-slide-deck path/to/article.md --audience executives
# With language
/slide-deck path/to/article.md --lang zh
/slide-deck path/to/article.md --lang en
/baoyu-slide-deck path/to/article.md --lang zh
/baoyu-slide-deck path/to/article.md --lang en
# Outline only (no image generation)
/slide-deck path/to/article.md --outline-only
/baoyu-slide-deck path/to/article.md --outline-only
# Direct content input
/slide-deck
/baoyu-slide-deck
[paste content]
```
@@ -131,7 +131,7 @@ path/to/
### Without Article Path
```
./slide-deck-outputs/YYYY-MM-DD/[topic-slug]/
./baoyu-slide-deck-outputs/YYYY-MM-DD/[topic-slug]/
├── outline.md
├── prompts/
│ ├── 01-cover.md
@@ -272,7 +272,7 @@ Style notes: [specific style characteristics to emphasize]
For each slide, generate using:
```bash
/gemini-web --promptfiles [SKILL_ROOT]/skills/slide-deck/prompts/system.md [TARGET_DIR]/prompts/01-cover.md --image [TARGET_DIR]/01-cover.png
/baoyu-gemini-web --promptfiles [SKILL_ROOT]/skills/baoyu-slide-deck/prompts/system.md [TARGET_DIR]/prompts/01-cover.md --image [TARGET_DIR]/01-cover.png
```
Generation flow:
@@ -1,5 +1,5 @@
---
name: xhs-images
name: baoyu-xhs-images
description: Xiaohongshu (Little Red Book) infographic series generator with multiple style options. Breaks down content into 1-10 cartoon-style infographics. Use when user asks to create "小红书图片", "XHS images", or "RedNote infographics".
---
@@ -11,23 +11,23 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
```bash
# Auto-select style and layout based on content
/xhs-images posts/ai-future/article.md
/baoyu-xhs-images posts/ai-future/article.md
# Specify style
/xhs-images posts/ai-future/article.md --style notion
/baoyu-xhs-images posts/ai-future/article.md --style notion
# Specify layout
/xhs-images posts/ai-future/article.md --layout dense
/baoyu-xhs-images posts/ai-future/article.md --layout dense
# Combine style and layout
/xhs-images posts/ai-future/article.md --style tech --layout list
/baoyu-xhs-images posts/ai-future/article.md --style tech --layout list
# Direct content input
/xhs-images
/baoyu-xhs-images
[paste content]
# Direct input with options
/xhs-images --style bold --layout comparison
/baoyu-xhs-images --style bold --layout comparison
[paste content]
```
-339
View File
@@ -1,339 +0,0 @@
---
name: post-to-x
description: Post content and articles to X (Twitter). Supports regular posts with images and X Articles (long-form Markdown). Uses real Chrome with CDP to bypass anti-automation.
---
# Post to X (Twitter)
Post content, images, and long-form articles to X using real Chrome browser (bypasses anti-bot detection).
## Features
- **Regular Posts**: Text + up to 4 images
- **X Articles**: Publish Markdown files with rich formatting and images (requires X Premium)
## Usage
```bash
# Post text only
/post-to-x "Your post content here"
# Post with image
/post-to-x "Your post content" --image /path/to/image.png
# Post with multiple images (up to 4)
/post-to-x "Your post content" --image img1.png --image img2.png
# Actually submit the post
/post-to-x "Your post content" --submit
```
## Prerequisites
- Google Chrome or Chromium installed
- `bun` installed (for running scripts)
- First run: log in to X in the opened browser window
## Quick Start (Recommended)
Use the `x-browser.ts` script directly:
```bash
# Preview mode (doesn't post)
npx -y bun ./scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
# Actually post
npx -y bun ./scripts/x-browser.ts "Hello!" --image ./photo.png --submit
```
The script:
1. Launches real Chrome with anti-detection disabled
2. Uses persistent profile (only need to log in once)
3. Types text and pastes images via CDP
4. Waits 30s for preview (or posts immediately with `--submit`)
## Manual Workflow
If you prefer step-by-step control:
### Step 1: Copy Image to Clipboard
```bash
npx -y bun ./scripts/copy-to-clipboard.ts image /path/to/image.png
```
### Step 2: Use Playwright MCP (if Chrome session available)
```bash
# Navigate
mcp__playwright__browser_navigate url="https://x.com/compose/post"
# Get element refs
mcp__playwright__browser_snapshot
# Type text
mcp__playwright__browser_click element="editor" ref="<ref>"
mcp__playwright__browser_type element="editor" ref="<ref>" text="Your content"
# Paste image (after copying to clipboard)
mcp__playwright__browser_press_key key="Meta+v" # macOS
# or
mcp__playwright__browser_press_key key="Control+v" # Windows/Linux
# Screenshot to verify
mcp__playwright__browser_take_screenshot filename="preview.png"
```
## Parameters
| Parameter | Description |
|-----------|-------------|
| `<text>` | Post content (positional argument) |
| `--image <path>` | Image file path (can be repeated, max 4) |
| `--submit` | Actually post (default: preview only) |
| `--profile <dir>` | Custom Chrome profile directory |
## Image Support
- Formats: PNG, JPEG, GIF, WebP
- Max 4 images per post
- Images copied to system clipboard, then pasted via keyboard shortcut
## Example Session
```
User: /post-to-x "Hello from Claude!" --image ./screenshot.png
Claude:
1. Runs: npx -y bun ./scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
2. Chrome opens with X compose page
3. Text is typed into editor
4. Image is copied to clipboard and pasted
5. Browser stays open 30s for preview
6. Reports: "Post composed. Use --submit to post."
```
## Troubleshooting
- **Chrome not found**: Set `X_BROWSER_CHROME_PATH` environment variable
- **Not logged in**: First run opens Chrome - log in manually, cookies are saved
- **Image paste fails**: Verify clipboard script: `npx -y bun ./scripts/copy-to-clipboard.ts image <path>`
- **Rate limited**: Wait a few minutes before retrying
## How It Works
The `x-browser.ts` script uses Chrome DevTools Protocol (CDP) to:
1. Launch real Chrome (not Playwright) with `--disable-blink-features=AutomationControlled`
2. Use persistent profile directory for saved login sessions
3. Interact with X via CDP commands (Runtime.evaluate, Input.dispatchKeyEvent)
4. Paste images from system clipboard
This approach bypasses X's anti-automation detection that blocks Playwright/Puppeteer.
## Notes
- First run requires manual login (session is saved)
- Always preview before using `--submit`
- Browser closes automatically after operation
- Supports macOS, Linux, and Windows
---
# X Articles (Long-form Publishing)
Publish Markdown articles to X Articles editor with rich text formatting and images.
## X Article Usage
```bash
# Publish markdown article (preview mode)
/post-to-x article /path/to/article.md
# With custom cover image
/post-to-x article article.md --cover ./hero.png
# With custom title
/post-to-x article article.md --title "My Custom Title"
# Actually publish (not just draft)
/post-to-x article article.md --submit
```
## Prerequisites for Articles
- X Premium subscription (required for Articles)
- Google Chrome installed
- `bun` installed
## Article Script
Use `x-article.ts` directly:
```bash
npx -y bun ./scripts/x-article.ts article.md
npx -y bun ./scripts/x-article.ts article.md --cover ./cover.jpg
npx -y bun ./scripts/x-article.ts article.md --submit
```
## Markdown Format
```markdown
---
title: My Article Title
cover_image: /path/to/cover.jpg
---
# Title (becomes article title)
Regular paragraph text with **bold** and *italic*.
## Section Header
More content here.
![Image alt text](./image.png)
- List item 1
- List item 2
1. Numbered item
2. Another item
> Blockquote text
[Link text](https://example.com)
\`\`\`
Code blocks become blockquotes (X doesn't support code)
\`\`\`
```
## Frontmatter Fields
| Field | Description |
|-------|-------------|
| `title` | Article title (or uses first H1) |
| `cover_image` | Cover image path or URL |
| `cover` | Alias for cover_image |
| `image` | Alias for cover_image |
## Image Handling
1. **Cover Image**: First image or `cover_image` from frontmatter
2. **Remote Images**: Automatically downloaded to temp directory
3. **Placeholders**: Images in content use `[[IMAGE_PLACEHOLDER_N]]` format
4. **Insertion**: Placeholders are found, selected, and replaced with actual images
## Markdown to HTML Script
Convert markdown and inspect structure:
```bash
# Get JSON with all metadata
npx -y bun ./scripts/md-to-html.ts article.md
# Output HTML only
npx -y bun ./scripts/md-to-html.ts article.md --html-only
# Save HTML to file
npx -y bun ./scripts/md-to-html.ts article.md --save-html /tmp/article.html
```
JSON output:
```json
{
"title": "Article Title",
"coverImage": "/path/to/cover.jpg",
"contentImages": [
{
"placeholder": "[[IMAGE_PLACEHOLDER_1]]",
"localPath": "/tmp/x-article-images/img.png",
"blockIndex": 5
}
],
"html": "<p>Content...</p>",
"totalBlocks": 20
}
```
## Supported Formatting
| Markdown | HTML Output |
|----------|-------------|
| `# H1` | Title only (not in body) |
| `## H2` - `###### H6` | `<h2>` |
| `**bold**` | `<strong>` |
| `*italic*` | `<em>` |
| `[text](url)` | `<a href>` |
| `> quote` | `<blockquote>` |
| `` `code` `` | `<code>` |
| ```` ``` ```` | `<blockquote>` (X limitation) |
| `- item` | `<ul><li>` |
| `1. item` | `<ol><li>` |
| `![](img)` | Image placeholder |
## Article Workflow
1. **Parse Markdown**: Extract title, cover, content images, generate HTML
2. **Launch Chrome**: Real browser with CDP, persistent login
3. **Navigate**: Open `x.com/compose/articles`
4. **Create Article**: Click create button if on list page
5. **Upload Cover**: Use file input for cover image
6. **Fill Title**: Type title into title field
7. **Paste Content**: Copy HTML to clipboard, paste into editor
8. **Insert Images**: For each placeholder (reverse order):
- Find placeholder text in editor
- Select the placeholder
- Copy image to clipboard
- Paste to replace selection
9. **Review**: Browser stays open for 60s preview
10. **Publish**: Only with `--submit` flag
## Article Example Session
```
User: /post-to-x article ./blog/my-post.md --cover ./thumbnail.png
Claude:
1. Parses markdown: title="My Post", 3 content images
2. Launches Chrome with CDP
3. Navigates to x.com/compose/articles
4. Clicks create button
5. Uploads thumbnail.png as cover
6. Fills title "My Post"
7. Pastes HTML content
8. Inserts 3 images at placeholder positions
9. Reports: "Article composed. Review and use --submit to publish."
```
## Article Troubleshooting
- **No create button**: Ensure X Premium subscription is active
- **Cover upload fails**: Check file path and format (PNG, JPEG)
- **Images not inserting**: Verify placeholders exist in pasted content
- **Content not pasting**: Check HTML clipboard: `npx -y bun ./scripts/copy-to-clipboard.ts html --file /tmp/test.html`
## How Article Publishing Works
1. `md-to-html.ts` converts Markdown to HTML:
- Extracts frontmatter (title, cover)
- Converts markdown to HTML
- Replaces images with unique placeholders
- Downloads remote images locally
- Returns structured JSON
2. `x-article.ts` publishes via CDP:
- Launches real Chrome (bypasses detection)
- Uses persistent profile (saved login)
- Navigates and fills editor via DOM manipulation
- Pastes HTML from system clipboard
- Finds/selects/replaces each image placeholder
## Scripts Reference
| Script | Purpose |
|--------|---------|
| `x-browser.ts` | Regular posts (text + images) |
| `x-article.ts` | Article publishing (Markdown) |
| `md-to-html.ts` | Markdown → HTML conversion |
| `copy-to-clipboard.ts` | Copy image/HTML to clipboard |