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": { "metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency", "description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "0.3.1" "version": "0.4.1"
}, },
"plugins": [ "plugins": [
{ {
@@ -15,13 +15,13 @@
"source": "./", "source": "./",
"strict": false, "strict": false,
"skills": [ "skills": [
"./skills/gemini-web", "./skills/baoyu-gemini-web",
"./skills/xhs-images", "./skills/baoyu-xhs-images",
"./skills/post-to-x", "./skills/baoyu-post-to-x",
"./skills/post-to-wechat", "./skills/baoyu-post-to-wechat",
"./skills/article-illustrator", "./skills/baoyu-article-illustrator",
"./skills/cover-image", "./skills/baoyu-cover-image",
"./skills/slide-deck" "./skills/baoyu-slide-deck"
] ]
} }
] ]
+20 -15
View File
@@ -10,12 +10,13 @@ Claude Code marketplace plugin providing AI-powered content generation skills. S
``` ```
skills/ skills/
├── gemini-web/ # Core: Gemini API wrapper (text + image gen) ├── baoyu-gemini-web/ # Core: Gemini API wrapper (text + image gen)
├── xhs-images/ # Xiaohongshu infographic series (1-10 images) ├── baoyu-xhs-images/ # Xiaohongshu infographic series (1-10 images)
├── cover-image/ # Article cover images (2.35:1 aspect) ├── baoyu-cover-image/ # Article cover images (2.35:1 aspect)
├── slide-deck/ # Presentation slides with outlines ├── baoyu-slide-deck/ # Presentation slides with outlines
├── article-illustrator/ # Smart illustration placement ├── baoyu-article-illustrator/ # Smart illustration placement
── post-to-x/ # X/Twitter posting automation ── baoyu-post-to-x/ # X/Twitter posting automation
└── baoyu-post-to-wechat/ # WeChat Official Account posting
``` ```
Each skill contains: Each skill contains:
@@ -34,24 +35,24 @@ npx -y bun skills/<skill>/scripts/main.ts [options]
Examples: Examples:
```bash ```bash
# Text generation # 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 # 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 # 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 ## Key Dependencies
- **Bun**: TypeScript runtime (via `npx -y bun`) - **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 - **No npm packages**: Self-contained TypeScript, no external dependencies
## Authentication ## Authentication
`gemini-web` uses browser cookies for Google auth: `baoyu-gemini-web` uses browser cookies for Google auth:
- First run opens Chrome for login - First run opens Chrome for login
- Cookies cached in data directory - Cookies cached in data directory
- Force refresh: `--login` flag - 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 ## Adding New Skills
1. Create `skills/<name>/SKILL.md` with YAML front matter **IMPORTANT**: All skills MUST use `baoyu-` prefix to avoid conflicts when users import this plugin.
2. Add TypeScript in `skills/<name>/scripts/`
3. Add prompt templates in `skills/<name>/prompts/` if needed 1. Create `skills/baoyu-<name>/SKILL.md` with YAML front matter
4. Register in `marketplace.json` plugins[0].skills array - 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 ## Code Style
+19
View File
@@ -34,6 +34,25 @@ Run the following command in Claude Code:
/plugin install content-skills@baoyu-skills /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 ## Available Skills
### gemini-web ### gemini-web
+19
View File
@@ -34,6 +34,25 @@
/plugin install content-skills@baoyu-skills /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 ### 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". 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 ```bash
# Auto-select style based on content # Auto-select style based on content
/article-illustrator path/to/article.md /baoyu-article-illustrator path/to/article.md
# Specify a style # Specify a style
/article-illustrator path/to/article.md --style tech /baoyu-article-illustrator path/to/article.md --style tech
/article-illustrator path/to/article.md --style warm /baoyu-article-illustrator path/to/article.md --style warm
/article-illustrator path/to/article.md --style minimal /baoyu-article-illustrator path/to/article.md --style minimal
# Combine with other options # Combine with other options
/article-illustrator path/to/article.md --style playful /baoyu-article-illustrator path/to/article.md --style playful
``` ```
## Options ## 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". 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 ```bash
# From markdown file (auto-select style based on content) # 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 # Specify a style
/cover-image path/to/article.md --style tech /baoyu-cover-image path/to/article.md --style tech
/cover-image path/to/article.md --style warm /baoyu-cover-image path/to/article.md --style warm
/cover-image path/to/article.md --style bold /baoyu-cover-image path/to/article.md --style bold
# Without title text # Without title text
/cover-image path/to/article.md --no-title /baoyu-cover-image path/to/article.md --no-title
# Combine options # 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 # From direct text input
/cover-image /baoyu-cover-image
[paste content or describe the topic] [paste content or describe the topic]
# Direct input with style # Direct input with style
/cover-image --style playful /baoyu-cover-image --style playful
[paste content] [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. 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?.( 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; return merged;
} }
@@ -124,7 +124,7 @@ export function createGeminiWebExecutor(
const cookieMap = await loadGeminiCookies(runOptions.config, log); const cookieMap = await loadGeminiCookies(runOptions.config, log);
if (!hasRequiredGeminiCookies(cookieMap)) { if (!hasRequiredGeminiCookies(cookieMap)) {
throw new Error( 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(); const profileDir = resolveGeminiWebChromeProfileDir();
console.log(`Usage: console.log(`Usage:
npx -y bun skills/gemini-web/scripts/main.ts --prompt "Hello" npx -y bun skills/baoyu-gemini-web/scripts/main.ts --prompt "Hello"
npx -y bun skills/gemini-web/scripts/main.ts "Hello" npx -y bun skills/baoyu-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/baoyu-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 --promptfiles system.md content.md --image out.png
Options: Options:
-p, --prompt <text> Prompt text -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 (图文). 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. 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 ## Quick Usage
### Image-Text (图文) - Multiple images with title/content ### Image-Text (图文) - Multiple images with title/content
```bash ```bash
# From markdown file and image directory # 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 # 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 ### Article (文章) - Full markdown with formatting
```bash ```bash
# Post markdown article # 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 ## References
- **Image-Text Posting**: See `references/image-text-posting.md` for detailed image-text posting guide - **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) // Resolve image paths (download remote, resolve relative)
const contentImages: ImageInfo[] = []; const contentImages: ImageInfo[] = [];
let isFirstImage = true; let isFirstImage = true;
let coverPlaceholder: string | null = null;
for (let i = 0; i < images.length; i++) { for (let i = 0; i < images.length; i++) {
const img = images[i]!; const img = images[i]!;
@@ -311,6 +312,7 @@ export async function parseMarkdown(
// First image becomes cover if no cover specified // First image becomes cover if no cover specified
if (isFirstImage && !coverImagePath) { if (isFirstImage && !coverImagePath) {
coverImagePath = localPath; coverImagePath = localPath;
coverPlaceholder = `[[IMAGE_PLACEHOLDER_${i + 1}]]`;
isFirstImage = false; isFirstImage = false;
// Don't add to contentImages, it's the cover // Don't add to contentImages, it's the cover
continue; 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 // Resolve cover image path
let resolvedCoverImage: string | null = null; let resolvedCoverImage: string | null = null;
if (coverImagePath) { if (coverImagePath) {
@@ -335,7 +344,7 @@ export async function parseMarkdown(
title, title,
coverImage: resolvedCoverImage, coverImage: resolvedCoverImage,
contentImages, contentImages,
html, html: finalHtml,
totalBlocks, 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 { function copyImageToClipboard(imagePath: string): boolean {
const scriptDir = path.dirname(new URL(import.meta.url).pathname); const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
const copyScript = path.join(scriptDir, 'copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' }); const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
return result.status === 0; return result.status === 0;
} }
function copyHtmlToClipboard(htmlPath: string): boolean { function copyHtmlToClipboard(htmlPath: string): boolean {
const scriptDir = path.dirname(new URL(import.meta.url).pathname); const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
const copyScript = path.join(scriptDir, 'copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'html', '--file', htmlPath], { stdio: 'inherit' }); const result = spawnSync('npx', ['-y', 'bun', copyScript, 'html', '--file', htmlPath], { stdio: 'inherit' });
return result.status === 0; return result.status === 0;
} }
function sendRealPasteKeystroke(retries = 3): boolean { function pasteFromClipboard(targetApp?: string, retries = 3, delayMs = 500): boolean {
if (process.platform !== 'darwin') { const pasteScript = path.join(getScriptDir(), 'paste-from-clipboard.ts');
console.log('[x-article] Real keystroke only supported on macOS'); const args = ['npx', '-y', 'bun', pasteScript, '--retries', String(retries), '--delay', String(delayMs)];
return false; if (targetApp) {
args.push('--app', targetApp);
} }
const result = spawnSync(args[0]!, args.slice(1), { stdio: 'inherit' });
// Use osascript to send Cmd+V to frontmost application (Chrome) return result.status === 0;
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;
} }
interface ArticleOptions { interface ArticleOptions {
@@ -300,11 +286,6 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
}, { sessionId }); }, { 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) // Check if we're on the articles list page (has Write button)
console.log('[x-article] Looking for Write button...'); console.log('[x-article] Looking for Write button...');
const writeButtonFound = await waitForElement('[data-testid="empty_state_button_text"]', 10_000); 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 }); }, { sessionId });
// Wait for scroll animation // Wait for scroll animation
await sleep(300); await sleep(500);
if (!placeholderFound.result.value) { if (!placeholderFound.result.value) {
console.warn(`[x-article] Placeholder not found in DOM: ${img.placeholder}`); console.warn(`[x-article] Placeholder not found in DOM: ${img.placeholder}`);
@@ -553,25 +534,26 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
continue; continue;
} }
await sleep(300); // Wait for clipboard to be fully ready
await sleep(800);
// Delete placeholder by pressing Enter (placeholder is already selected) // Delete placeholder by pressing Enter (placeholder is already selected)
console.log(`[x-article] Deleting placeholder with Enter...`); 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: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, { sessionId });
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', 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...`); console.log(`[x-article] Pasting image...`);
if (sendRealPasteKeystroke()) { if (pasteFromClipboard('Google Chrome', 5, 800)) {
console.log(`[x-article] Image pasted: ${path.basename(img.localPath)}`); console.log(`[x-article] Image pasted: ${path.basename(img.localPath)}`);
} else { } else {
console.warn(`[x-article] Failed to paste image`); console.warn(`[x-article] Failed to paste image after retries`);
} }
// Wait for image to upload // Wait for image to upload
console.log(`[x-article] Waiting for upload...`); console.log(`[x-article] Waiting for upload...`);
await sleep(4000); await sleep(5000);
} }
console.log('[x-article] All images processed.'); 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!'); console.log('[x-article] Article published!');
} else { } else {
console.log('[x-article] Article composed (draft mode).'); console.log('[x-article] Article composed (draft mode).');
console.log('[x-article] Browser will stay open for 60 seconds for review...'); console.log('[x-article] Browser remains open for manual review.');
await sleep(60_000);
} }
} finally { } finally {
// Disconnect CDP but keep browser open
if (cdp) { if (cdp) {
try { await cdp.send('Browser.close', {}, { timeoutMs: 5_000 }); } catch {}
cdp.close(); cdp.close();
} }
setTimeout(() => { if (!chrome.killed) try { chrome.kill('SIGKILL'); } catch {} }, 2_000).unref?.(); // Don't kill Chrome - let user review and close manually
try { chrome.kill('SIGTERM'); } catch {}
} }
} }
@@ -8,6 +8,26 @@ import process from 'node:process';
const X_COMPOSE_URL = 'https://x.com/compose/post'; 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> { function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); 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}`); console.log(`[x-browser] Pasting image: ${imagePath}`);
const scriptDir = path.dirname(new URL(import.meta.url).pathname); if (!copyImageToClipboard(imagePath)) {
const copyScript = path.join(scriptDir, 'copy-to-clipboard.ts');
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
if (result.status !== 0) {
console.warn(`[x-browser] Failed to copy image to clipboard: ${imagePath}`); console.warn(`[x-browser] Failed to copy image to clipboard: ${imagePath}`);
continue; continue;
} }
// Wait for clipboard to be ready
await sleep(500);
// Focus the editor
await cdp.send('Runtime.evaluate', { await cdp.send('Runtime.evaluate', {
expression: `document.querySelector('[data-testid="tweetTextarea_0"]')?.focus()`, expression: `document.querySelector('[data-testid="tweetTextarea_0"]')?.focus()`,
}, { sessionId }); }, { sessionId });
await sleep(200);
const modifiers = process.platform === 'darwin' ? 4 : 2; // Use paste script (handles platform differences, activates Chrome)
await cdp.send('Input.dispatchKeyEvent', { console.log('[x-browser] Pasting from clipboard...');
type: 'keyDown', const pasteSuccess = pasteFromClipboard('Google Chrome', 5, 500);
key: 'v',
code: 'KeyV', if (!pasteSuccess) {
modifiers, // Fallback to CDP (may not work for images on X)
windowsVirtualKeyCode: 86, console.log('[x-browser] Paste script failed, trying CDP fallback...');
}, { sessionId }); const modifiers = process.platform === 'darwin' ? 4 : 2;
await cdp.send('Input.dispatchKeyEvent', { await cdp.send('Input.dispatchKeyEvent', {
type: 'keyUp', type: 'keyDown',
key: 'v', key: 'v',
code: 'KeyV', code: 'KeyV',
modifiers, modifiers,
windowsVirtualKeyCode: 86, windowsVirtualKeyCode: 86,
}, { sessionId }); }, { sessionId });
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyUp',
key: 'v',
code: 'KeyV',
modifiers,
windowsVirtualKeyCode: 86,
}, { sessionId });
}
console.log('[x-browser] Waiting for image upload...'); console.log('[x-browser] Waiting for image upload...');
await sleep(3000); await sleep(4000);
} }
if (submit) { 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". 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 ```bash
# From markdown file # From markdown file
/slide-deck path/to/article.md /baoyu-slide-deck path/to/article.md
# With style preference # With style preference
/slide-deck path/to/article.md --style corporate /baoyu-slide-deck path/to/article.md --style corporate
/slide-deck path/to/article.md --style playful /baoyu-slide-deck path/to/article.md --style playful
/slide-deck path/to/article.md --style technical /baoyu-slide-deck path/to/article.md --style technical
# With audience specification # With audience specification
/slide-deck path/to/article.md --audience beginners /baoyu-slide-deck path/to/article.md --audience beginners
/slide-deck path/to/article.md --audience executives /baoyu-slide-deck path/to/article.md --audience executives
# With language # With language
/slide-deck path/to/article.md --lang zh /baoyu-slide-deck path/to/article.md --lang zh
/slide-deck path/to/article.md --lang en /baoyu-slide-deck path/to/article.md --lang en
# Outline only (no image generation) # 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 # Direct content input
/slide-deck /baoyu-slide-deck
[paste content] [paste content]
``` ```
@@ -131,7 +131,7 @@ path/to/
### Without Article Path ### Without Article Path
``` ```
./slide-deck-outputs/YYYY-MM-DD/[topic-slug]/ ./baoyu-slide-deck-outputs/YYYY-MM-DD/[topic-slug]/
├── outline.md ├── outline.md
├── prompts/ ├── prompts/
│ ├── 01-cover.md │ ├── 01-cover.md
@@ -272,7 +272,7 @@ Style notes: [specific style characteristics to emphasize]
For each slide, generate using: For each slide, generate using:
```bash ```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: 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". 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 ```bash
# Auto-select style and layout based on content # 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 # Specify style
/xhs-images posts/ai-future/article.md --style notion /baoyu-xhs-images posts/ai-future/article.md --style notion
# Specify layout # 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 # 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 # Direct content input
/xhs-images /baoyu-xhs-images
[paste content] [paste content]
# Direct input with options # Direct input with options
/xhs-images --style bold --layout comparison /baoyu-xhs-images --style bold --layout comparison
[paste content] [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 |