mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-10 21:21:30 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3d2f5d03f | |||
| da920bb830 |
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## Available Skills
|
||||
|
||||
### gemini-web
|
||||
|
||||
@@ -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** 启用自动更新,每次启动时自动获取最新版本。
|
||||
|
||||

|
||||
|
||||
## 可用技能
|
||||
|
||||
### 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();
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
- 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>` |
|
||||
| `` | 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.
|
||||
+10
-1
@@ -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();
|
||||
+24
-44
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+50
-21
@@ -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]
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
- 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>` |
|
||||
| `` | 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 |
|
||||
Reference in New Issue
Block a user