mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-16 23:49:47 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce259e4547 | |||
| fff1a54b6b | |||
| 9437581c48 | |||
| bd4db203f8 | |||
| 51387498a5 | |||
| 6d1336d8bd | |||
| 42931d6294 | |||
| 4c9e57777b | |||
| 210905ef66 |
@@ -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": "1.43.2"
|
"version": "1.46.0"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,31 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.46.0 - 2026-03-05
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Add `--output-dir` option to url-to-markdown for custom output directory with auto-generated filenames
|
||||||
|
|
||||||
|
## 1.45.1 - 2026-03-05
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
- Replace hardcoded `npx -y bun` with `${BUN_X}` runtime variable across all skills — prefers native `bun`, falls back to `npx -y bun`
|
||||||
|
- Add Runtime Detection section to CLAUDE.md and Script Directory instructions in all SKILL.md files
|
||||||
|
|
||||||
|
## 1.45.0 - 2026-03-05
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-post-to-x`: add post-composition verification for X Articles — automatically checks remaining placeholders and image count after all images are inserted
|
||||||
|
- `baoyu-post-to-x`: increase CDP timeout to 60s and add 3s DOM stabilization delay between image insertions for long articles
|
||||||
|
|
||||||
|
## 1.44.0 - 2026-03-05
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-url-to-markdown`: add `--download-media` flag to download images and videos to local directories, rewriting markdown links to local paths
|
||||||
|
- `baoyu-url-to-markdown`: extract cover image from page meta (og:image) into YAML front matter `coverImage` field
|
||||||
|
- `baoyu-url-to-markdown`: handle `data-src` lazy loading for WeChat and similar sites
|
||||||
|
- `baoyu-url-to-markdown`: add EXTEND.md preferences with first-time setup for media download behavior
|
||||||
|
|
||||||
## 1.43.2 - 2026-03-05
|
## 1.43.2 - 2026-03-05
|
||||||
|
|
||||||
### Refactor
|
### Refactor
|
||||||
|
|||||||
@@ -2,6 +2,31 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.46.0 - 2026-03-05
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- 为 url-to-markdown 新增 `--output-dir` 选项,支持自定义输出目录并自动生成文件名
|
||||||
|
|
||||||
|
## 1.45.1 - 2026-03-05
|
||||||
|
|
||||||
|
### 重构
|
||||||
|
- 将所有技能中硬编码的 `npx -y bun` 替换为 `${BUN_X}` 运行时变量——优先使用原生 `bun`,回退到 `npx -y bun`
|
||||||
|
- 在 CLAUDE.md 中新增运行时检测章节,在所有 SKILL.md 的脚本目录说明中添加运行时解析步骤
|
||||||
|
|
||||||
|
## 1.45.0 - 2026-03-05
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-post-to-x`:X 文章发布后自动验证——检查残留占位符和图片数量是否正确
|
||||||
|
- `baoyu-post-to-x`:增加 CDP 超时至 60 秒,图片插入间隔增加 3 秒 DOM 稳定等待,改善长文章发布稳定性
|
||||||
|
|
||||||
|
## 1.44.0 - 2026-03-05
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-url-to-markdown`:新增 `--download-media` 参数,支持下载图片和视频到本地目录,并将 Markdown 中的链接改写为本地路径
|
||||||
|
- `baoyu-url-to-markdown`:从页面 meta 信息(og:image)提取封面图,写入 YAML front matter 的 `coverImage` 字段
|
||||||
|
- `baoyu-url-to-markdown`:支持 `data-src` 懒加载图片提取(兼容微信公众号等站点)
|
||||||
|
- `baoyu-url-to-markdown`:新增 EXTEND.md 偏好设置,支持首次使用引导配置媒体下载行为
|
||||||
|
|
||||||
## 1.43.2 - 2026-03-05
|
## 1.43.2 - 2026-03-05
|
||||||
|
|
||||||
### 重构
|
### 重构
|
||||||
|
|||||||
@@ -43,27 +43,51 @@ Each skill contains:
|
|||||||
|
|
||||||
## Running Skills
|
## Running Skills
|
||||||
|
|
||||||
All scripts run via Bun (no build step):
|
All scripts are TypeScript, executed via Bun runtime (no build step).
|
||||||
|
|
||||||
|
### Runtime Detection (`${BUN_X}`)
|
||||||
|
|
||||||
|
Before running any script, the agent MUST detect the runtime **once per session** and set `${BUN_X}`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun skills/<skill>/scripts/main.ts [options]
|
# Detect runtime (run once, reuse result)
|
||||||
|
if command -v bun &>/dev/null; then
|
||||||
|
BUN_X="bun"
|
||||||
|
elif command -v npx &>/dev/null; then
|
||||||
|
BUN_X="npx -y bun"
|
||||||
|
else
|
||||||
|
echo "Error: Neither bun nor npx found. Install bun: curl -fsSL https://bun.sh/install | bash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
| Priority | Condition | `${BUN_X}` value | Notes |
|
||||||
|
|----------|-----------|-------------------|-------|
|
||||||
|
| 1 | `bun` installed | `bun` | Fastest, native execution |
|
||||||
|
| 2 | `npx` available | `npx -y bun` | Downloads bun on first run via npm |
|
||||||
|
| 3 | Neither found | Error + install guide | Suggest: `curl -fsSL https://bun.sh/install \| bash` |
|
||||||
|
|
||||||
|
### Script Execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
${BUN_X} skills/<skill>/scripts/main.ts [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
```bash
|
```bash
|
||||||
# Text generation
|
# Text generation
|
||||||
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts "Hello"
|
${BUN_X} skills/baoyu-danger-gemini-web/scripts/main.ts "Hello"
|
||||||
|
|
||||||
# Image generation
|
# Image generation
|
||||||
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --prompt "A cat" --image cat.png
|
${BUN_X} skills/baoyu-danger-gemini-web/scripts/main.ts --prompt "A cat" --image cat.png
|
||||||
|
|
||||||
# From prompt files
|
# From prompt files
|
||||||
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
|
${BUN_X} skills/baoyu-danger-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 (native `bun` preferred, fallback `npx -y bun`)
|
||||||
- **Chrome**: Required for `baoyu-danger-gemini-web` auth and `baoyu-post-to-x` automation
|
- **Chrome**: Required for `baoyu-danger-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
|
||||||
|
|
||||||
@@ -176,7 +200,8 @@ Every SKILL.md with scripts MUST include this section after Usage:
|
|||||||
**Agent Execution Instructions**:
|
**Agent Execution Instructions**:
|
||||||
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
||||||
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
||||||
3. Replace all `${SKILL_DIR}` in this document with the actual path
|
3. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
4. Replace all `${SKILL_DIR}` and `${BUN_X}` in this document with actual values
|
||||||
|
|
||||||
**Script Reference**:
|
**Script Reference**:
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
@@ -185,7 +210,7 @@ Every SKILL.md with scripts MUST include this section after Usage:
|
|||||||
| `scripts/other.ts` | Other functionality |
|
| `scripts/other.ts` | Other functionality |
|
||||||
```
|
```
|
||||||
|
|
||||||
When referencing scripts in workflow sections, use `${SKILL_DIR}/scripts/<name>.ts` so agents can resolve the correct path.
|
When referencing scripts in workflow sections, use `${BUN_X} ${SKILL_DIR}/scripts/<name>.ts` so agents can resolve the correct runtime and path.
|
||||||
|
|
||||||
### Progressive Disclosure
|
### Progressive Disclosure
|
||||||
|
|
||||||
@@ -320,13 +345,13 @@ When adding, updating, or deleting styles for `baoyu-comic`, follow this workflo
|
|||||||
- Add auto-selection entry if applicable
|
- Add auto-selection entry if applicable
|
||||||
3. **Generate showcase image**:
|
3. **Generate showcase image**:
|
||||||
```bash
|
```bash
|
||||||
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts \
|
${BUN_X} skills/baoyu-danger-gemini-web/scripts/main.ts \
|
||||||
--prompt "A single comic book page in <style-name> style showing [appropriate scene]. Features: [style characteristics from style definition]. 3:4 portrait aspect ratio comic page." \
|
--prompt "A single comic book page in <style-name> style showing [appropriate scene]. Features: [style characteristics from style definition]. 3:4 portrait aspect ratio comic page." \
|
||||||
--image screenshots/comic-styles/<style-name>.png
|
--image screenshots/comic-styles/<style-name>.png
|
||||||
```
|
```
|
||||||
4. **Compress to WebP**:
|
4. **Compress to WebP**:
|
||||||
```bash
|
```bash
|
||||||
npx -y bun skills/baoyu-compress-image/scripts/main.ts screenshots/comic-styles/<style-name>.png
|
${BUN_X} skills/baoyu-compress-image/scripts/main.ts screenshots/comic-styles/<style-name>.png
|
||||||
```
|
```
|
||||||
5. **Update both READMEs** (`README.md` and `README.zh.md`):
|
5. **Update both READMEs** (`README.md` and `README.zh.md`):
|
||||||
- Add style to `--style` options
|
- Add style to `--style` options
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ Details: [references/auto-selection.md](references/auto-selection.md)
|
|||||||
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
||||||
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
||||||
3. Replace all `${SKILL_DIR}` in this document with the actual path
|
3. Replace all `${SKILL_DIR}` in this document with the actual path
|
||||||
|
4. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
|
||||||
**Script Reference**:
|
**Script Reference**:
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
@@ -209,7 +210,7 @@ Analyze → [Check Existing?] → [Confirm: Style + Reviews] → Storyboard →
|
|||||||
- **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
|
- **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
|
||||||
```bash
|
```bash
|
||||||
# Use Reference Sheet Prompt from characters/characters.md
|
# Use Reference Sheet Prompt from characters/characters.md
|
||||||
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
${BUN_X} ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
||||||
--promptfiles characters/characters.md \
|
--promptfiles characters/characters.md \
|
||||||
--image characters/characters.png --ar 4:3
|
--image characters/characters.png --ar 4:3
|
||||||
```
|
```
|
||||||
@@ -233,7 +234,7 @@ Compress to reduce token usage when used as reference image:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example: ALWAYS include --ref for consistency
|
# Example: ALWAYS include --ref for consistency
|
||||||
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
${BUN_X} ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
||||||
--promptfiles prompts/01-page-xxx.md \
|
--promptfiles prompts/01-page-xxx.md \
|
||||||
--image 01-page-xxx.png --ar 3:4 \
|
--image 01-page-xxx.png --ar 3:4 \
|
||||||
--ref characters/characters.png
|
--ref characters/characters.png
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ With confirmed prompts from Step 5/6:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Each page generation MUST include --ref
|
# Each page generation MUST include --ref
|
||||||
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
${BUN_X} ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
||||||
--promptfiles prompts/01-page-xxx.md \
|
--promptfiles prompts/01-page-xxx.md \
|
||||||
--image 01-page-xxx.png \
|
--image 01-page-xxx.png \
|
||||||
--ar 3:4 \
|
--ar 3:4 \
|
||||||
@@ -474,7 +474,7 @@ If image generation skill supports `--sessionId`:
|
|||||||
After all images generated:
|
After all images generated:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/merge-to-pdf.ts <comic-dir>
|
${BUN_X} ${SKILL_DIR}/scripts/merge-to-pdf.ts <comic-dir>
|
||||||
```
|
```
|
||||||
|
|
||||||
Creates `{topic-slug}.pdf` with all pages as full-page images.
|
Creates `{topic-slug}.pdf` with all pages as full-page images.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Compresses images using best available tool (sips → cwebp → ImageMagick →
|
|||||||
|
|
||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
Scripts in `scripts/` subdirectory. Replace `${SKILL_DIR}` with this SKILL.md's directory path.
|
Scripts in `scripts/` subdirectory. `${SKILL_DIR}` = this SKILL.md's directory path. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `${SKILL_DIR}` and `${BUN_X}` with actual values.
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
@@ -48,7 +48,7 @@ test -f "$HOME/.baoyu-skills/baoyu-compress-image/EXTEND.md" && echo "user"
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts <input> [options]
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <input> [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
@@ -67,16 +67,16 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts <input> [options]
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Single file → WebP (replaces original)
|
# Single file → WebP (replaces original)
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts image.png
|
||||||
|
|
||||||
# Keep PNG format
|
# Keep PNG format
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png -f png --keep
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts image.png -f png --keep
|
||||||
|
|
||||||
# Directory recursive
|
# Directory recursive
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts ./images/ -r -q 75
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts ./images/ -r -q 75
|
||||||
|
|
||||||
# JSON output
|
# JSON output
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png --json
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts image.png --json
|
||||||
```
|
```
|
||||||
|
|
||||||
**Output**:
|
**Output**:
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ Text/image generation via Gemini Web API. Supports reference images and multi-tu
|
|||||||
**Agent Execution Instructions**:
|
**Agent Execution Instructions**:
|
||||||
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
||||||
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
||||||
3. Replace all `${SKILL_DIR}` in this document with the actual path
|
3. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
4. Replace all `${SKILL_DIR}` and `${BUN_X}` in this document with actual values
|
||||||
|
|
||||||
**Script Reference**:
|
**Script Reference**:
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
@@ -75,23 +76,23 @@ test -f "$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md" && echo "user"
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Text generation
|
# Text generation
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts "Your prompt"
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts "Your prompt"
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Your prompt" --model gemini-3-flash
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Your prompt" --model gemini-3-flash
|
||||||
|
|
||||||
# Image generation
|
# Image generation
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cute cat" --image cat.png
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cute cat" --image cat.png
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||||
|
|
||||||
# Vision input (reference images)
|
# Vision input (reference images)
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Describe this" --reference image.png
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Describe this" --reference image.png
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Create variation" --reference a.png --image out.png
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Create variation" --reference a.png --image out.png
|
||||||
|
|
||||||
# Multi-turn conversation
|
# Multi-turn conversation
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts "Remember: 42" --sessionId session-abc
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts "Remember: 42" --sessionId session-abc
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts "What number?" --sessionId session-abc
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts "What number?" --sessionId session-abc
|
||||||
|
|
||||||
# JSON output
|
# JSON output
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts "Hello" --json
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts "Hello" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Scripts located in `scripts/` subdirectory.
|
|||||||
**Path Resolution**:
|
**Path Resolution**:
|
||||||
1. `SKILL_DIR` = this SKILL.md's directory
|
1. `SKILL_DIR` = this SKILL.md's directory
|
||||||
2. Script path = `${SKILL_DIR}/scripts/main.ts`
|
2. Script path = `${SKILL_DIR}/scripts/main.ts`
|
||||||
|
3. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
|
||||||
## Consent Requirement
|
## Consent Requirement
|
||||||
|
|
||||||
@@ -135,10 +136,10 @@ Full reference: [references/config/first-time-setup.md](references/config/first-
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts <url>
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url>
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts <url> -o output.md
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> -o output.md
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts <url> --download-media
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --download-media
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts <url> --json
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Transforms plain text or markdown files into well-structured markdown with prope
|
|||||||
|
|
||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
Scripts in `scripts/` subdirectory. Replace `${SKILL_DIR}` with this SKILL.md's directory path.
|
Scripts in `scripts/` subdirectory. `${SKILL_DIR}` = this SKILL.md's directory path. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `${SKILL_DIR}` and `${BUN_X}` with actual values.
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
@@ -210,7 +210,7 @@ Example:
|
|||||||
After saving, **must** run the formatting script:
|
After saving, **must** run the formatting script:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts {output-file-path} [options]
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts {output-file-path} [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Script Options:**
|
**Script Options:**
|
||||||
@@ -229,16 +229,16 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts {output-file-path} [options]
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Default: spacing + emphasis enabled, quotes disabled
|
# Default: spacing + emphasis enabled, quotes disabled
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md
|
||||||
|
|
||||||
# Enable all features including quote replacement
|
# Enable all features including quote replacement
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --quotes
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --quotes
|
||||||
|
|
||||||
# Only fix emphasis issues, skip spacing
|
# Only fix emphasis issues, skip spacing
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --no-spacing
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --no-spacing
|
||||||
|
|
||||||
# Disable all processing except frontmatter formatting
|
# Disable all processing except frontmatter formatting
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --no-spacing --no-emphasis
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --no-spacing --no-emphasis
|
||||||
```
|
```
|
||||||
|
|
||||||
**Script performs (based on options):**
|
**Script performs (based on options):**
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Official API-based image generation. Supports OpenAI, Google, DashScope (阿里
|
|||||||
**Agent Execution**:
|
**Agent Execution**:
|
||||||
1. `SKILL_DIR` = this SKILL.md file's directory
|
1. `SKILL_DIR` = this SKILL.md file's directory
|
||||||
2. Script path = `${SKILL_DIR}/scripts/main.ts`
|
2. Script path = `${SKILL_DIR}/scripts/main.ts`
|
||||||
|
3. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
|
||||||
## Step 0: Load Preferences ⛔ BLOCKING
|
## Step 0: Load Preferences ⛔ BLOCKING
|
||||||
|
|
||||||
@@ -44,34 +45,34 @@ Schema: `references/config/preferences-schema.md`
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Basic
|
# Basic
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image cat.png
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image cat.png
|
||||||
|
|
||||||
# With aspect ratio
|
# With aspect ratio
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A landscape" --image out.png --ar 16:9
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A landscape" --image out.png --ar 16:9
|
||||||
|
|
||||||
# High quality
|
# High quality
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --quality 2k
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --quality 2k
|
||||||
|
|
||||||
# From prompt files
|
# From prompt files
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||||
|
|
||||||
# With reference images (Google multimodal or OpenAI edits)
|
# With reference images (Google multimodal or OpenAI edits)
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
|
||||||
|
|
||||||
# With reference images (explicit provider/model)
|
# With reference images (explicit provider/model)
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --provider google --model gemini-3-pro-image-preview --ref source.png
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --provider google --model gemini-3-pro-image-preview --ref source.png
|
||||||
|
|
||||||
# Specific provider
|
# Specific provider
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider openai
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider openai
|
||||||
|
|
||||||
# DashScope (阿里通义万象)
|
# DashScope (阿里通义万象)
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "一只可爱的猫" --image out.png --provider dashscope
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "一只可爱的猫" --image out.png --provider dashscope
|
||||||
|
|
||||||
# Replicate (google/nano-banana-pro)
|
# Replicate (google/nano-banana-pro)
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
|
||||||
|
|
||||||
# Replicate with specific model
|
# Replicate with specific model
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
@@ -136,10 +137,10 @@ Examples:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Use Replicate default model
|
# Use Replicate default model
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
|
||||||
|
|
||||||
# Override model explicitly
|
# Override model explicitly
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
|
||||||
```
|
```
|
||||||
|
|
||||||
## Provider Selection
|
## Provider Selection
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Converts Markdown files to beautifully styled HTML with inline CSS, optimized fo
|
|||||||
|
|
||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
**Agent Execution**: Determine this SKILL.md directory as `SKILL_DIR`, then use `${SKILL_DIR}/scripts/<name>.ts`.
|
**Agent Execution**: Determine this SKILL.md directory as `SKILL_DIR`. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `${SKILL_DIR}` and `${BUN_X}` with actual values.
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
@@ -97,7 +97,7 @@ test -f "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md" && grep -o 'default
|
|||||||
### Step 2: Convert
|
### Step 2: Convert
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts <markdown_file> --theme <theme>
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <markdown_file> --theme <theme>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Report Result
|
### Step 3: Report Result
|
||||||
@@ -107,7 +107,7 @@ Display the output path from JSON result. If backup was created, mention it.
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts <markdown_file> [options]
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <markdown_file> [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
@@ -144,19 +144,19 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts <markdown_file> [options]
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Basic conversion (uses default theme, removes first heading)
|
# Basic conversion (uses default theme, removes first heading)
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md
|
||||||
|
|
||||||
# With specific theme
|
# With specific theme
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --theme grace
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --theme grace
|
||||||
|
|
||||||
# Theme with custom color
|
# Theme with custom color
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --theme modern --color red
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --theme modern --color red
|
||||||
|
|
||||||
# Keep the first heading in content
|
# Keep the first heading in content
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --keep-title
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --keep-title
|
||||||
|
|
||||||
# Override title
|
# Override title
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --title "My Article"
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --title "My Article"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ description: Posts content to WeChat Official Account (微信公众号) via API
|
|||||||
|
|
||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
**Agent Execution**: Determine this SKILL.md directory as `SKILL_DIR`, then use `${SKILL_DIR}/scripts/<name>.ts`.
|
**Agent Execution**: Determine this SKILL.md directory as `SKILL_DIR`, then use `${SKILL_DIR}/scripts/<name>.ts`. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun.
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
@@ -88,7 +88,7 @@ chrome_profile_path: /path/to/chrome/profile
|
|||||||
Before first use, suggest running the environment check. User can skip if they prefer.
|
Before first use, suggest running the environment check. User can skip if they prefer.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/check-permissions.ts
|
${BUN_X} ${SKILL_DIR}/scripts/check-permissions.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystroke, API credentials, Chrome conflicts.
|
Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystroke, API credentials, Chrome conflicts.
|
||||||
@@ -111,8 +111,8 @@ Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystrok
|
|||||||
For short posts with multiple images (up to 9):
|
For short posts with multiple images (up to 9):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/wechat-browser.ts --markdown article.md --images ./images/
|
${BUN_X} ${SKILL_DIR}/scripts/wechat-browser.ts --markdown article.md --images ./images/
|
||||||
npx -y bun ${SKILL_DIR}/scripts/wechat-browser.ts --title "标题" --content "内容" --image img.png --submit
|
${BUN_X} ${SKILL_DIR}/scripts/wechat-browser.ts --title "标题" --content "内容" --image img.png --submit
|
||||||
```
|
```
|
||||||
|
|
||||||
See [references/image-text-posting.md](references/image-text-posting.md) for details.
|
See [references/image-text-posting.md](references/image-text-posting.md) for details.
|
||||||
@@ -247,7 +247,7 @@ WECHAT_APP_SECRET=<user_input>
|
|||||||
**API method** (accepts `.md` or `.html`):
|
**API method** (accepts `.md` or `.html`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/wechat-api.ts <file> --theme <theme> [--color <color>] [--title <title>] [--summary <summary>] [--author <author>] [--cover <cover_path>]
|
${BUN_X} ${SKILL_DIR}/scripts/wechat-api.ts <file> --theme <theme> [--color <color>] [--title <title>] [--summary <summary>] [--author <author>] [--cover <cover_path>]
|
||||||
```
|
```
|
||||||
|
|
||||||
**CRITICAL**: Always include `--theme` parameter. Never omit it, even if using `default`. Only include `--color` if explicitly set by user or EXTEND.md.
|
**CRITICAL**: Always include `--theme` parameter. Never omit it, even if using `default`. Only include `--color` if explicitly set by user or EXTEND.md.
|
||||||
@@ -266,8 +266,8 @@ If script parameters do not expose the two comment fields, still ensure final AP
|
|||||||
**Browser method** (accepts `--markdown` or `--html`):
|
**Browser method** (accepts `--markdown` or `--html`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/wechat-article.ts --markdown <markdown_file> --theme <theme> [--color <color>]
|
${BUN_X} ${SKILL_DIR}/scripts/wechat-article.ts --markdown <markdown_file> --theme <theme> [--color <color>]
|
||||||
npx -y bun ${SKILL_DIR}/scripts/wechat-article.ts --html <html_file>
|
${BUN_X} ${SKILL_DIR}/scripts/wechat-article.ts --html <html_file>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 5: Completion Report
|
### Step 5: Completion Report
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ Post markdown articles to WeChat Official Account with full formatting support.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Post markdown article
|
# Post markdown article
|
||||||
npx -y bun ./scripts/wechat-article.ts --markdown article.md
|
${BUN_X} ./scripts/wechat-article.ts --markdown article.md
|
||||||
|
|
||||||
# With theme
|
# With theme
|
||||||
npx -y bun ./scripts/wechat-article.ts --markdown article.md --theme grace
|
${BUN_X} ./scripts/wechat-article.ts --markdown article.md --theme grace
|
||||||
|
|
||||||
# With explicit options
|
# With explicit options
|
||||||
npx -y bun ./scripts/wechat-article.ts --markdown article.md --author "作者名" --summary "摘要"
|
${BUN_X} ./scripts/wechat-article.ts --markdown article.md --author "作者名" --summary "摘要"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ Post image-text messages with multiple images to WeChat Official Account.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Post with images and markdown file (title/content extracted automatically)
|
# Post with images and markdown file (title/content extracted automatically)
|
||||||
npx -y bun ./scripts/wechat-browser.ts --markdown source.md --images ./images/
|
${BUN_X} ./scripts/wechat-browser.ts --markdown source.md --images ./images/
|
||||||
|
|
||||||
# Post with explicit title and content
|
# Post with explicit title and content
|
||||||
npx -y bun ./scripts/wechat-browser.ts --title "标题" --content "内容" --image img1.png --image img2.png
|
${BUN_X} ./scripts/wechat-browser.ts --title "标题" --content "内容" --image img1.png --image img2.png
|
||||||
|
|
||||||
# Save as draft
|
# Save as draft
|
||||||
npx -y bun ./scripts/wechat-browser.ts --markdown source.md --images ./images/ --submit
|
${BUN_X} ./scripts/wechat-browser.ts --markdown source.md --images ./images/ --submit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Posts text, images, videos, and long-form articles to X via real Chrome browser
|
|||||||
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
||||||
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
||||||
3. Replace all `${SKILL_DIR}` in this document with the actual path
|
3. Replace all `${SKILL_DIR}` in this document with the actual path
|
||||||
|
4. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
|
||||||
**Script Reference**:
|
**Script Reference**:
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
@@ -69,7 +70,7 @@ test -f "$HOME/.baoyu-skills/baoyu-post-to-x/EXTEND.md" && echo "user"
|
|||||||
Before first use, suggest running the environment check. User can skip if they prefer.
|
Before first use, suggest running the environment check. User can skip if they prefer.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/check-paste-permissions.ts
|
${BUN_X} ${SKILL_DIR}/scripts/check-paste-permissions.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystroke, Chrome conflicts.
|
Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystroke, Chrome conflicts.
|
||||||
@@ -98,7 +99,7 @@ Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystrok
|
|||||||
Text + up to 4 images.
|
Text + up to 4 images.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/x-browser.ts "Hello!" --image ./photo.png
|
${BUN_X} ${SKILL_DIR}/scripts/x-browser.ts "Hello!" --image ./photo.png
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters**:
|
**Parameters**:
|
||||||
@@ -117,7 +118,7 @@ npx -y bun ${SKILL_DIR}/scripts/x-browser.ts "Hello!" --image ./photo.png
|
|||||||
Text + video file.
|
Text + video file.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/x-video.ts "Check this out!" --video ./clip.mp4
|
${BUN_X} ${SKILL_DIR}/scripts/x-video.ts "Check this out!" --video ./clip.mp4
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters**:
|
**Parameters**:
|
||||||
@@ -138,7 +139,7 @@ npx -y bun ${SKILL_DIR}/scripts/x-video.ts "Check this out!" --video ./clip.mp4
|
|||||||
Quote an existing tweet with comment.
|
Quote an existing tweet with comment.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/x-quote.ts https://x.com/user/status/123 "Great insight!"
|
${BUN_X} ${SKILL_DIR}/scripts/x-quote.ts https://x.com/user/status/123 "Great insight!"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters**:
|
**Parameters**:
|
||||||
@@ -157,8 +158,8 @@ npx -y bun ${SKILL_DIR}/scripts/x-quote.ts https://x.com/user/status/123 "Great
|
|||||||
Long-form Markdown articles (requires X Premium).
|
Long-form Markdown articles (requires X Premium).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md
|
${BUN_X} ${SKILL_DIR}/scripts/x-article.ts article.md
|
||||||
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
|
${BUN_X} ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters**:
|
**Parameters**:
|
||||||
@@ -172,6 +173,12 @@ npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
|
|||||||
|
|
||||||
**Note**: Script opens browser with article filled in. User reviews and publishes manually.
|
**Note**: Script opens browser with article filled in. User reviews and publishes manually.
|
||||||
|
|
||||||
|
**Post-Composition Check**: The script automatically verifies after all images are inserted:
|
||||||
|
- Remaining `XIMGPH_` placeholders in editor content
|
||||||
|
- Expected vs actual image count
|
||||||
|
|
||||||
|
If the check fails (warnings in output), alert the user with the specific issues before they publish.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ Publish Markdown articles to X Articles editor with rich text formatting and ima
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Publish markdown article (preview mode)
|
# Publish markdown article (preview mode)
|
||||||
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md
|
${BUN_X} ${SKILL_DIR}/scripts/x-article.ts article.md
|
||||||
|
|
||||||
# With custom cover image
|
# With custom cover image
|
||||||
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
|
${BUN_X} ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
|
||||||
|
|
||||||
# Actually publish
|
# Actually publish
|
||||||
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --submit
|
${BUN_X} ${SKILL_DIR}/scripts/x-article.ts article.md --submit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Markdown Format
|
## Markdown Format
|
||||||
@@ -76,13 +76,13 @@ Convert markdown and inspect structure:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Get JSON with all metadata
|
# Get JSON with all metadata
|
||||||
npx -y bun ${SKILL_DIR}/scripts/md-to-html.ts article.md
|
${BUN_X} ${SKILL_DIR}/scripts/md-to-html.ts article.md
|
||||||
|
|
||||||
# Output HTML only
|
# Output HTML only
|
||||||
npx -y bun ${SKILL_DIR}/scripts/md-to-html.ts article.md --html-only
|
${BUN_X} ${SKILL_DIR}/scripts/md-to-html.ts article.md --html-only
|
||||||
|
|
||||||
# Save HTML to file
|
# Save HTML to file
|
||||||
npx -y bun ${SKILL_DIR}/scripts/md-to-html.ts article.md --save-html /tmp/article.html
|
${BUN_X} ${SKILL_DIR}/scripts/md-to-html.ts article.md --save-html /tmp/article.html
|
||||||
```
|
```
|
||||||
|
|
||||||
JSON output:
|
JSON output:
|
||||||
@@ -132,8 +132,12 @@ JSON output:
|
|||||||
- Select the placeholder
|
- Select the placeholder
|
||||||
- Copy image to clipboard
|
- Copy image to clipboard
|
||||||
- Paste to replace selection
|
- Paste to replace selection
|
||||||
9. **Review**: Browser stays open for 60s preview
|
9. **Post-Composition Check** (automatic):
|
||||||
10. **Publish**: Only with `--submit` flag
|
- Scan editor for remaining `XIMGPH_` placeholders
|
||||||
|
- Compare expected vs actual image count
|
||||||
|
- Warn if issues found
|
||||||
|
10. **Review**: Browser stays open for 60s preview
|
||||||
|
11. **Publish**: Only with `--submit` flag
|
||||||
|
|
||||||
## Example Session
|
## Example Session
|
||||||
|
|
||||||
@@ -157,7 +161,7 @@ Claude:
|
|||||||
- **No create button**: Ensure X Premium subscription is active
|
- **No create button**: Ensure X Premium subscription is active
|
||||||
- **Cover upload fails**: Check file path and format (PNG, JPEG)
|
- **Cover upload fails**: Check file path and format (PNG, JPEG)
|
||||||
- **Images not inserting**: Verify placeholders exist in pasted content
|
- **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`
|
- **Content not pasting**: Check HTML clipboard: `${BUN_X} ${SKILL_DIR}/scripts/copy-to-clipboard.ts html --file /tmp/test.html`
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
|
|||||||
@@ -9,20 +9,20 @@ If you prefer step-by-step control:
|
|||||||
### Step 1: Copy Image to Clipboard
|
### Step 1: Copy Image to Clipboard
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/copy-to-clipboard.ts image /path/to/image.png
|
${BUN_X} ${SKILL_DIR}/scripts/copy-to-clipboard.ts image /path/to/image.png
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Paste from Clipboard
|
### Step 2: Paste from Clipboard
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Simple paste to frontmost app
|
# Simple paste to frontmost app
|
||||||
npx -y bun ${SKILL_DIR}/scripts/paste-from-clipboard.ts
|
${BUN_X} ${SKILL_DIR}/scripts/paste-from-clipboard.ts
|
||||||
|
|
||||||
# Paste to Chrome with retries
|
# Paste to Chrome with retries
|
||||||
npx -y bun ${SKILL_DIR}/scripts/paste-from-clipboard.ts --app "Google Chrome" --retries 5
|
${BUN_X} ${SKILL_DIR}/scripts/paste-from-clipboard.ts --app "Google Chrome" --retries 5
|
||||||
|
|
||||||
# Quick paste with shorter delay
|
# Quick paste with shorter delay
|
||||||
npx -y bun ${SKILL_DIR}/scripts/paste-from-clipboard.ts --delay 200
|
${BUN_X} ${SKILL_DIR}/scripts/paste-from-clipboard.ts --delay 200
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Use Playwright MCP (if Chrome session available)
|
### Step 3: Use Playwright MCP (if Chrome session available)
|
||||||
@@ -59,7 +59,7 @@ mcp__playwright__browser_take_screenshot filename="preview.png"
|
|||||||
User: /post-to-x "Hello from Claude!" --image ./screenshot.png
|
User: /post-to-x "Hello from Claude!" --image ./screenshot.png
|
||||||
|
|
||||||
Claude:
|
Claude:
|
||||||
1. Runs: npx -y bun ${SKILL_DIR}/scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
|
1. Runs: ${BUN_X} ${SKILL_DIR}/scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
|
||||||
2. Chrome opens with X compose page
|
2. Chrome opens with X compose page
|
||||||
3. Text is typed into editor
|
3. Text is typed into editor
|
||||||
4. Image is copied to clipboard and pasted
|
4. Image is copied to clipboard and pasted
|
||||||
@@ -72,7 +72,7 @@ Claude:
|
|||||||
- **Chrome not found**: Set `X_BROWSER_CHROME_PATH` environment variable
|
- **Chrome not found**: Set `X_BROWSER_CHROME_PATH` environment variable
|
||||||
- **Not logged in**: First run opens Chrome - log in manually, cookies are saved
|
- **Not logged in**: First run opens Chrome - log in manually, cookies are saved
|
||||||
- **Image paste fails**:
|
- **Image paste fails**:
|
||||||
- Verify clipboard script: `npx -y bun ${SKILL_DIR}/scripts/copy-to-clipboard.ts image <path>`
|
- Verify clipboard script: `${BUN_X} ${SKILL_DIR}/scripts/copy-to-clipboard.ts image <path>`
|
||||||
- On macOS, grant "Accessibility" permission to Terminal/iTerm in System Settings > Privacy & Security > Accessibility
|
- On macOS, grant "Accessibility" permission to Terminal/iTerm in System Settings > Privacy & Security > Accessibility
|
||||||
- Keep Chrome window visible and in front during paste operations
|
- Keep Chrome window visible and in front during paste operations
|
||||||
- **osascript permission denied**: Grant Terminal accessibility permissions in System Preferences
|
- **osascript permission denied**: Grant Terminal accessibility permissions in System Preferences
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const wsUrl = await waitForChromeDebugPort(port, 30_000, { includeLastError: true });
|
const wsUrl = await waitForChromeDebugPort(port, 30_000, { includeLastError: true });
|
||||||
cdp = await CdpConnection.connect(wsUrl, 30_000, { defaultTimeoutMs: 30_000 });
|
cdp = await CdpConnection.connect(wsUrl, 30_000, { defaultTimeoutMs: 60_000 });
|
||||||
|
|
||||||
// Get page target
|
// Get page target
|
||||||
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
|
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
|
||||||
@@ -148,7 +148,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
await cdp.send('DOM.enable', {}, { sessionId });
|
await cdp.send('DOM.enable', {}, { sessionId });
|
||||||
|
|
||||||
console.log('[x-article] Waiting for articles page...');
|
console.log('[x-article] Waiting for articles page...');
|
||||||
await sleep(3000);
|
await sleep(1000);
|
||||||
|
|
||||||
// Wait for and click "create" button
|
// Wait for and click "create" button
|
||||||
const waitForElement = async (selector: string, timeoutMs = 60_000): Promise<boolean> => {
|
const waitForElement = async (selector: string, timeoutMs = 60_000): Promise<boolean> => {
|
||||||
@@ -644,6 +644,8 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
|
|
||||||
if (imgUploadOk) {
|
if (imgUploadOk) {
|
||||||
console.log(`[x-article] Image upload verified (${expectedImgCount} image block(s))`);
|
console.log(`[x-article] Image upload verified (${expectedImgCount} image block(s))`);
|
||||||
|
// Wait for DraftEditor DOM to stabilize after image insertion
|
||||||
|
await sleep(3000);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[x-article] Image upload not detected after 15s`);
|
console.warn(`[x-article] Image upload not detected after 15s`);
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
@@ -653,6 +655,42 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[x-article] All images processed.');
|
console.log('[x-article] All images processed.');
|
||||||
|
|
||||||
|
// Final verification: check placeholder residue and image count
|
||||||
|
console.log('[x-article] Running post-composition verification...');
|
||||||
|
const finalEditorContent = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||||
|
expression: `document.querySelector('.DraftEditor-editorContainer [data-contents="true"]')?.innerText || ''`,
|
||||||
|
returnByValue: true,
|
||||||
|
}, { sessionId });
|
||||||
|
|
||||||
|
const remainingPlaceholders: string[] = [];
|
||||||
|
for (const img of parsed.contentImages) {
|
||||||
|
const regex = new RegExp(img.placeholder + '(?!\\d)');
|
||||||
|
if (regex.test(finalEditorContent.result.value)) {
|
||||||
|
remainingPlaceholders.push(img.placeholder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalImgCount = await cdp.send<{ result: { value: number } }>('Runtime.evaluate', {
|
||||||
|
expression: `document.querySelectorAll('section[data-block="true"][contenteditable="false"] img[src^="blob:"]').length`,
|
||||||
|
returnByValue: true,
|
||||||
|
}, { sessionId });
|
||||||
|
|
||||||
|
const expectedCount = parsed.contentImages.length;
|
||||||
|
const actualCount = finalImgCount.result.value;
|
||||||
|
|
||||||
|
if (remainingPlaceholders.length > 0 || actualCount < expectedCount) {
|
||||||
|
console.warn('[x-article] ⚠ POST-COMPOSITION CHECK FAILED:');
|
||||||
|
if (remainingPlaceholders.length > 0) {
|
||||||
|
console.warn(`[x-article] Remaining placeholders: ${remainingPlaceholders.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (actualCount < expectedCount) {
|
||||||
|
console.warn(`[x-article] Image count: expected ${expectedCount}, found ${actualCount}`);
|
||||||
|
}
|
||||||
|
console.warn('[x-article] Please check the article before publishing.');
|
||||||
|
} else {
|
||||||
|
console.log(`[x-article] ✓ Verification passed: ${actualCount} image(s), no remaining placeholders.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before preview: blur editor to trigger save
|
// Before preview: blur editor to trigger save
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ Transform content into professional slide deck images.
|
|||||||
**Agent Execution Instructions**:
|
**Agent Execution Instructions**:
|
||||||
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
||||||
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
||||||
|
3. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
@@ -550,8 +551,8 @@ options:
|
|||||||
### Step 8: Merge to PPTX and PDF
|
### Step 8: Merge to PPTX and PDF
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/merge-to-pptx.ts <slide-deck-dir>
|
${BUN_X} ${SKILL_DIR}/scripts/merge-to-pptx.ts <slide-deck-dir>
|
||||||
npx -y bun ${SKILL_DIR}/scripts/merge-to-pdf.ts <slide-deck-dir>
|
${BUN_X} ${SKILL_DIR}/scripts/merge-to-pdf.ts <slide-deck-dir>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 9: Output Summary
|
### Step 9: Output Summary
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ Fetches any URL via Chrome CDP and converts HTML to clean markdown.
|
|||||||
**Agent Execution Instructions**:
|
**Agent Execution Instructions**:
|
||||||
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
||||||
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
||||||
3. Replace all `${SKILL_DIR}` in this document with the actual path
|
3. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
4. Replace all `${SKILL_DIR}` and `${BUN_X}` in this document with actual values
|
||||||
|
|
||||||
**Script Reference**:
|
**Script Reference**:
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
@@ -46,10 +47,51 @@ test -f "$HOME/.baoyu-skills/baoyu-url-to-markdown/EXTEND.md" && echo "user"
|
|||||||
├───────────┼───────────────────────────────────────────────────────────────────────────┤
|
├───────────┼───────────────────────────────────────────────────────────────────────────┤
|
||||||
│ Found │ Read, parse, apply settings │
|
│ Found │ Read, parse, apply settings │
|
||||||
├───────────┼───────────────────────────────────────────────────────────────────────────┤
|
├───────────┼───────────────────────────────────────────────────────────────────────────┤
|
||||||
│ Not found │ Use defaults │
|
│ Not found │ **MUST** run first-time setup (see below) — do NOT silently create defaults │
|
||||||
└───────────┴───────────────────────────────────────────────────────────────────────────┘
|
└───────────┴───────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
**EXTEND.md Supports**: Default output directory | Default capture mode | Timeout settings
|
**EXTEND.md Supports**: Download media by default | Default output directory | Default capture mode | Timeout settings
|
||||||
|
|
||||||
|
### First-Time Setup (BLOCKING)
|
||||||
|
|
||||||
|
**CRITICAL**: When EXTEND.md is not found, you **MUST use `AskUserQuestion`** to ask the user for their preferences before creating EXTEND.md. **NEVER** create EXTEND.md with defaults without asking. This is a **BLOCKING** operation — do NOT proceed with any conversion until setup is complete.
|
||||||
|
|
||||||
|
Use `AskUserQuestion` with ALL questions in ONE call:
|
||||||
|
|
||||||
|
**Question 1** — header: "Media", question: "How to handle images and videos in pages?"
|
||||||
|
- "Ask each time (Recommended)" — After saving markdown, ask whether to download media
|
||||||
|
- "Always download" — Always download media to local imgs/ and videos/ directories
|
||||||
|
- "Never download" — Keep original remote URLs in markdown
|
||||||
|
|
||||||
|
**Question 2** — header: "Output", question: "Default output directory?"
|
||||||
|
- "url-to-markdown (Recommended)" — Save to ./url-to-markdown/{domain}/{slug}.md
|
||||||
|
- (User may choose "Other" to type a custom path)
|
||||||
|
|
||||||
|
**Question 3** — header: "Save", question: "Where to save preferences?"
|
||||||
|
- "User (Recommended)" — ~/.baoyu-skills/ (all projects)
|
||||||
|
- "Project" — .baoyu-skills/ (this project only)
|
||||||
|
|
||||||
|
After user answers, create EXTEND.md at the chosen location, confirm "Preferences saved to [path]", then continue.
|
||||||
|
|
||||||
|
Full reference: [references/config/first-time-setup.md](references/config/first-time-setup.md)
|
||||||
|
|
||||||
|
### Supported Keys
|
||||||
|
|
||||||
|
| Key | Default | Values | Description |
|
||||||
|
|-----|---------|--------|-------------|
|
||||||
|
| `download_media` | `ask` | `ask` / `1` / `0` | `ask` = prompt each time, `1` = always download, `0` = never |
|
||||||
|
| `default_output_dir` | empty | path or empty | Default output directory (empty = `./url-to-markdown/`) |
|
||||||
|
|
||||||
|
**EXTEND.md → CLI mapping**:
|
||||||
|
| EXTEND.md key | CLI argument | Notes |
|
||||||
|
|---------------|-------------|-------|
|
||||||
|
| `download_media: 1` | `--download-media` | |
|
||||||
|
| `default_output_dir: ./posts/` | `--output-dir ./posts/` | Directory path. Do NOT pass to `-o` (which expects a file path) |
|
||||||
|
|
||||||
|
**Value priority**:
|
||||||
|
1. CLI arguments (`--download-media`, `-o`, `--output-dir`)
|
||||||
|
2. EXTEND.md
|
||||||
|
3. Skill defaults
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -57,18 +99,25 @@ test -f "$HOME/.baoyu-skills/baoyu-url-to-markdown/EXTEND.md" && echo "user"
|
|||||||
- Two capture modes: auto or wait-for-user
|
- Two capture modes: auto or wait-for-user
|
||||||
- Clean markdown output with metadata
|
- Clean markdown output with metadata
|
||||||
- Handles login-required pages via wait mode
|
- Handles login-required pages via wait mode
|
||||||
|
- Download images and videos to local directories
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Auto mode (default) - capture when page loads
|
# Auto mode (default) - capture when page loads
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts <url>
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url>
|
||||||
|
|
||||||
# Wait mode - wait for user signal before capture
|
# Wait mode - wait for user signal before capture
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts <url> --wait
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --wait
|
||||||
|
|
||||||
# Save to specific file
|
# Save to specific file
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts <url> -o output.md
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> -o output.md
|
||||||
|
|
||||||
|
# Save to a custom output directory (auto-generates filename)
|
||||||
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --output-dir ./posts/
|
||||||
|
|
||||||
|
# Download images and videos to local directories
|
||||||
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --download-media
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
@@ -76,9 +125,11 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts <url> -o output.md
|
|||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `<url>` | URL to fetch |
|
| `<url>` | URL to fetch |
|
||||||
| `-o <path>` | Output file path (default: auto-generated) |
|
| `-o <path>` | Output file path — must be a **file** path, not directory (default: auto-generated) |
|
||||||
|
| `--output-dir <dir>` | Base output directory — auto-generates `{dir}/{domain}/{slug}.md` (default: `./url-to-markdown/`) |
|
||||||
| `--wait` | Wait for user signal before capturing |
|
| `--wait` | Wait for user signal before capturing |
|
||||||
| `--timeout <ms>` | Page load timeout (default: 30000) |
|
| `--timeout <ms>` | Page load timeout (default: 30000) |
|
||||||
|
| `--download-media` | Download image/video assets to local `imgs/` and `videos/`, and rewrite markdown links to local relative paths |
|
||||||
|
|
||||||
## Capture Modes
|
## Capture Modes
|
||||||
|
|
||||||
@@ -98,13 +149,38 @@ YAML front matter with `url`, `title`, `description`, `author`, `published`, `ca
|
|||||||
|
|
||||||
## Output Directory
|
## Output Directory
|
||||||
|
|
||||||
```
|
Default: `url-to-markdown/<domain>/<slug>.md`
|
||||||
url-to-markdown/<domain>/<slug>.md
|
With `--output-dir ./posts/`: `./posts/<domain>/<slug>.md`
|
||||||
```
|
|
||||||
|
|
||||||
- `<slug>`: From page title or URL path (kebab-case, 2-6 words)
|
- `<slug>`: From page title or URL path (kebab-case, 2-6 words)
|
||||||
- Conflict resolution: Append timestamp `<slug>-YYYYMMDD-HHMMSS.md`
|
- Conflict resolution: Append timestamp `<slug>-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
|
When `--download-media` is enabled:
|
||||||
|
- Images are saved to `imgs/` next to the markdown file
|
||||||
|
- Videos are saved to `videos/` next to the markdown file
|
||||||
|
- Markdown media links are rewritten to local relative paths
|
||||||
|
|
||||||
|
## Media Download Workflow
|
||||||
|
|
||||||
|
Based on `download_media` setting in EXTEND.md:
|
||||||
|
|
||||||
|
| Setting | Behavior |
|
||||||
|
|---------|----------|
|
||||||
|
| `1` (always) | Run script with `--download-media` flag |
|
||||||
|
| `0` (never) | Run script without `--download-media` flag |
|
||||||
|
| `ask` (default) | Follow the ask-each-time flow below |
|
||||||
|
|
||||||
|
### Ask-Each-Time Flow
|
||||||
|
|
||||||
|
1. Run script **without** `--download-media` → markdown saved
|
||||||
|
2. Check saved markdown for remote media URLs (`https://` in image/video links)
|
||||||
|
3. **If no remote media found** → done, no prompt needed
|
||||||
|
4. **If remote media found** → use `AskUserQuestion`:
|
||||||
|
- header: "Media", question: "Download N images/videos to local files?"
|
||||||
|
- "Yes" — Download to local directories
|
||||||
|
- "No" — Keep remote URLs
|
||||||
|
5. If user confirms → run script **again** with `--download-media` (overwrites markdown with localized links)
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
name: first-time-setup
|
||||||
|
description: First-time setup flow for baoyu-url-to-markdown preferences
|
||||||
|
---
|
||||||
|
|
||||||
|
# First-Time Setup
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
When no EXTEND.md is found, guide user through preference setup.
|
||||||
|
|
||||||
|
**BLOCKING OPERATION**: This setup MUST complete before ANY other workflow steps. Do NOT:
|
||||||
|
- Start converting URLs
|
||||||
|
- Ask about URLs or output paths
|
||||||
|
- Proceed to any conversion
|
||||||
|
|
||||||
|
ONLY ask the questions in this setup flow, save EXTEND.md, then continue.
|
||||||
|
|
||||||
|
## Setup Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
No EXTEND.md found
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+---------------------+
|
||||||
|
| AskUserQuestion |
|
||||||
|
| (all questions) |
|
||||||
|
+---------------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+---------------------+
|
||||||
|
| Create EXTEND.md |
|
||||||
|
+---------------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Continue conversion
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions
|
||||||
|
|
||||||
|
**Language**: Use user's input language or saved language preference.
|
||||||
|
|
||||||
|
Use AskUserQuestion with ALL questions in ONE call:
|
||||||
|
|
||||||
|
### Question 1: Download Media
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
header: "Media"
|
||||||
|
question: "How to handle images and videos in pages?"
|
||||||
|
options:
|
||||||
|
- label: "Ask each time (Recommended)"
|
||||||
|
description: "After saving markdown, ask whether to download media"
|
||||||
|
- label: "Always download"
|
||||||
|
description: "Always download media to local imgs/ and videos/ directories"
|
||||||
|
- label: "Never download"
|
||||||
|
description: "Keep original remote URLs in markdown"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Question 2: Default Output Directory
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
header: "Output"
|
||||||
|
question: "Default output directory?"
|
||||||
|
options:
|
||||||
|
- label: "url-to-markdown (Recommended)"
|
||||||
|
description: "Save to ./url-to-markdown/{domain}/{slug}.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: User will likely choose "Other" to type a custom path.
|
||||||
|
|
||||||
|
### Question 3: Save Location
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
header: "Save"
|
||||||
|
question: "Where to save preferences?"
|
||||||
|
options:
|
||||||
|
- label: "User (Recommended)"
|
||||||
|
description: "~/.baoyu-skills/ (all projects)"
|
||||||
|
- label: "Project"
|
||||||
|
description: ".baoyu-skills/ (this project only)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Save Locations
|
||||||
|
|
||||||
|
| Choice | Path | Scope |
|
||||||
|
|--------|------|-------|
|
||||||
|
| User | `~/.baoyu-skills/baoyu-url-to-markdown/EXTEND.md` | All projects |
|
||||||
|
| Project | `.baoyu-skills/baoyu-url-to-markdown/EXTEND.md` | Current project |
|
||||||
|
|
||||||
|
## After Setup
|
||||||
|
|
||||||
|
1. Create directory if needed
|
||||||
|
2. Write EXTEND.md
|
||||||
|
3. Confirm: "Preferences saved to [path]"
|
||||||
|
4. Continue with conversion using saved preferences
|
||||||
|
|
||||||
|
## EXTEND.md Template
|
||||||
|
|
||||||
|
```md
|
||||||
|
download_media: [ask/1/0]
|
||||||
|
default_output_dir: [path or empty]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modifying Preferences Later
|
||||||
|
|
||||||
|
Users can edit EXTEND.md directly or delete it to trigger setup again.
|
||||||
@@ -7,6 +7,7 @@ export interface PageMetadata {
|
|||||||
description?: string;
|
description?: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
published?: string;
|
published?: string;
|
||||||
|
coverImage?: string;
|
||||||
captured_at: string;
|
captured_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +40,12 @@ export const absolutizeUrlsScript = String.raw`
|
|||||||
}).filter(Boolean).join(", "));
|
}).filter(Boolean).join(", "));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
document.querySelectorAll("img[data-src], video[data-src], audio[data-src], source[data-src]").forEach(el => {
|
||||||
|
const ds = el.getAttribute("data-src");
|
||||||
|
if (ds && (!el.getAttribute("src") || el.getAttribute("src") === "" || el.getAttribute("src")?.startsWith("data:"))) {
|
||||||
|
el.setAttribute("src", ds);
|
||||||
|
}
|
||||||
|
});
|
||||||
absAttr("a[href]", "href");
|
absAttr("a[href]", "href");
|
||||||
absAttr("img[src], video[src], audio[src], source[src]", "src");
|
absAttr("img[src], video[src], audio[src], source[src]", "src");
|
||||||
absSrcset("img[srcset], source[srcset]");
|
absSrcset("img[srcset], source[srcset]");
|
||||||
@@ -56,6 +63,7 @@ export async function extractContent(html: string, url: string): Promise<Convers
|
|||||||
description: result.description || undefined,
|
description: result.description || undefined,
|
||||||
author: result.author || undefined,
|
author: result.author || undefined,
|
||||||
published: result.published || undefined,
|
published: result.published || undefined,
|
||||||
|
coverImage: result.image || undefined,
|
||||||
captured_at: new Date().toISOString(),
|
captured_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,6 +81,7 @@ export function formatMetadataYaml(meta: PageMetadata): string {
|
|||||||
if (meta.description) lines.push(`description: "${escapeYamlValue(meta.description)}"`);
|
if (meta.description) lines.push(`description: "${escapeYamlValue(meta.description)}"`);
|
||||||
if (meta.author) lines.push(`author: "${escapeYamlValue(meta.author)}"`);
|
if (meta.author) lines.push(`author: "${escapeYamlValue(meta.author)}"`);
|
||||||
if (meta.published) lines.push(`published: "${escapeYamlValue(meta.published)}"`);
|
if (meta.published) lines.push(`published: "${escapeYamlValue(meta.published)}"`);
|
||||||
|
if (meta.coverImage) lines.push(`coverImage: "${escapeYamlValue(meta.coverImage)}"`);
|
||||||
lines.push(`captured_at: "${escapeYamlValue(meta.captured_at)}"`);
|
lines.push(`captured_at: "${escapeYamlValue(meta.captured_at)}"`);
|
||||||
lines.push("---");
|
lines.push("---");
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import process from "node:process";
|
|||||||
|
|
||||||
import { CdpConnection, getFreePort, launchChrome, waitForChromeDebugPort, waitForNetworkIdle, waitForPageLoad, autoScroll, evaluateScript, killChrome } from "./cdp.js";
|
import { CdpConnection, getFreePort, launchChrome, waitForChromeDebugPort, waitForNetworkIdle, waitForPageLoad, autoScroll, evaluateScript, killChrome } from "./cdp.js";
|
||||||
import { absolutizeUrlsScript, extractContent, createMarkdownDocument, type ConversionResult } from "./html-to-markdown.js";
|
import { absolutizeUrlsScript, extractContent, createMarkdownDocument, type ConversionResult } from "./html-to-markdown.js";
|
||||||
|
import { localizeMarkdownMedia, countRemoteMedia } from "./media-localizer.js";
|
||||||
import { resolveUrlToMarkdownDataDir } from "./paths.js";
|
import { resolveUrlToMarkdownDataDir } from "./paths.js";
|
||||||
import { DEFAULT_TIMEOUT_MS, CDP_CONNECT_TIMEOUT_MS, NETWORK_IDLE_TIMEOUT_MS, POST_LOAD_DELAY_MS, SCROLL_STEP_WAIT_MS, SCROLL_MAX_STEPS } from "./constants.js";
|
import { DEFAULT_TIMEOUT_MS, CDP_CONNECT_TIMEOUT_MS, NETWORK_IDLE_TIMEOUT_MS, POST_LOAD_DELAY_MS, SCROLL_STEP_WAIT_MS, SCROLL_MAX_STEPS } from "./constants.js";
|
||||||
|
|
||||||
@@ -24,12 +25,14 @@ async function fileExists(filePath: string): Promise<boolean> {
|
|||||||
interface Args {
|
interface Args {
|
||||||
url: string;
|
url: string;
|
||||||
output?: string;
|
output?: string;
|
||||||
|
outputDir?: string;
|
||||||
wait: boolean;
|
wait: boolean;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
|
downloadMedia: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(argv: string[]): Args {
|
function parseArgs(argv: string[]): Args {
|
||||||
const args: Args = { url: "", wait: false, timeout: DEFAULT_TIMEOUT_MS };
|
const args: Args = { url: "", wait: false, timeout: DEFAULT_TIMEOUT_MS, downloadMedia: false };
|
||||||
for (let i = 2; i < argv.length; i++) {
|
for (let i = 2; i < argv.length; i++) {
|
||||||
const arg = argv[i];
|
const arg = argv[i];
|
||||||
if (arg === "--wait" || arg === "-w") {
|
if (arg === "--wait" || arg === "-w") {
|
||||||
@@ -38,6 +41,10 @@ function parseArgs(argv: string[]): Args {
|
|||||||
args.output = argv[++i];
|
args.output = argv[++i];
|
||||||
} else if (arg === "--timeout" || arg === "-t") {
|
} else if (arg === "--timeout" || arg === "-t") {
|
||||||
args.timeout = parseInt(argv[++i], 10) || DEFAULT_TIMEOUT_MS;
|
args.timeout = parseInt(argv[++i], 10) || DEFAULT_TIMEOUT_MS;
|
||||||
|
} else if (arg === "--output-dir") {
|
||||||
|
args.outputDir = argv[++i];
|
||||||
|
} else if (arg === "--download-media") {
|
||||||
|
args.downloadMedia = true;
|
||||||
} else if (!arg.startsWith("-") && !args.url) {
|
} else if (!arg.startsWith("-") && !args.url) {
|
||||||
args.url = arg;
|
args.url = arg;
|
||||||
}
|
}
|
||||||
@@ -62,10 +69,10 @@ function formatTimestamp(): string {
|
|||||||
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateOutputPath(url: string, title: string): Promise<string> {
|
async function generateOutputPath(url: string, title: string, outputDir?: string): Promise<string> {
|
||||||
const domain = new URL(url).hostname.replace(/^www\./, "");
|
const domain = new URL(url).hostname.replace(/^www\./, "");
|
||||||
const slug = generateSlug(title, url);
|
const slug = generateSlug(title, url);
|
||||||
const dataDir = resolveUrlToMarkdownDataDir();
|
const dataDir = outputDir ? path.resolve(outputDir) : resolveUrlToMarkdownDataDir();
|
||||||
const basePath = path.join(dataDir, domain, `${slug}.md`);
|
const basePath = path.join(dataDir, domain, `${slug}.md`);
|
||||||
|
|
||||||
if (!(await fileExists(basePath))) {
|
if (!(await fileExists(basePath))) {
|
||||||
@@ -145,15 +152,40 @@ async function main(): Promise<void> {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.output) {
|
||||||
|
const stat = await import("node:fs").then(fs => fs.statSync(args.output!, { throwIfNoEntry: false }));
|
||||||
|
if (stat?.isDirectory()) {
|
||||||
|
console.error(`Error: -o path is a directory, not a file: ${args.output}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Fetching: ${args.url}`);
|
console.log(`Fetching: ${args.url}`);
|
||||||
console.log(`Mode: ${args.wait ? "wait" : "auto"}`);
|
console.log(`Mode: ${args.wait ? "wait" : "auto"}`);
|
||||||
|
|
||||||
const result = await captureUrl(args);
|
const result = await captureUrl(args);
|
||||||
const outputPath = args.output || await generateOutputPath(args.url, result.metadata.title);
|
const outputPath = args.output || await generateOutputPath(args.url, result.metadata.title, args.outputDir);
|
||||||
const outputDir = path.dirname(outputPath);
|
const outputDir = path.dirname(outputPath);
|
||||||
await mkdir(outputDir, { recursive: true });
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
const document = createMarkdownDocument(result);
|
let document = createMarkdownDocument(result);
|
||||||
|
|
||||||
|
if (args.downloadMedia) {
|
||||||
|
const mediaResult = await localizeMarkdownMedia(document, {
|
||||||
|
markdownPath: outputPath,
|
||||||
|
log: console.log,
|
||||||
|
});
|
||||||
|
document = mediaResult.markdown;
|
||||||
|
if (mediaResult.downloadedImages > 0 || mediaResult.downloadedVideos > 0) {
|
||||||
|
console.log(`Downloaded: ${mediaResult.downloadedImages} images, ${mediaResult.downloadedVideos} videos`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { images, videos } = countRemoteMedia(document);
|
||||||
|
if (images > 0 || videos > 0) {
|
||||||
|
console.log(`Remote media found: ${images} images, ${videos} videos`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await writeFile(outputPath, document, "utf-8");
|
await writeFile(outputPath, document, "utf-8");
|
||||||
|
|
||||||
console.log(`Saved: ${outputPath}`);
|
console.log(`Saved: ${outputPath}`);
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
type MediaKind = "image" | "video";
|
||||||
|
type MediaHint = "image" | "unknown";
|
||||||
|
|
||||||
|
type MarkdownLinkCandidate = {
|
||||||
|
url: string;
|
||||||
|
hint: MediaHint;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LocalizeMarkdownMediaOptions = {
|
||||||
|
markdownPath: string;
|
||||||
|
log?: (message: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LocalizeMarkdownMediaResult = {
|
||||||
|
markdown: string;
|
||||||
|
downloadedImages: number;
|
||||||
|
downloadedVideos: number;
|
||||||
|
imageDir: string | null;
|
||||||
|
videoDir: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MARKDOWN_LINK_RE = /(!?\[[^\]\n]*\])\((<)?(https?:\/\/[^)\s>]+)(>)?\)/g;
|
||||||
|
const FRONTMATTER_COVER_RE = /^(coverImage:\s*")(https?:\/\/[^"]+)(")/m;
|
||||||
|
|
||||||
|
const IMAGE_EXTENSIONS = new Set([
|
||||||
|
"jpg",
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"webp",
|
||||||
|
"gif",
|
||||||
|
"bmp",
|
||||||
|
"avif",
|
||||||
|
"heic",
|
||||||
|
"heif",
|
||||||
|
"svg",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const VIDEO_EXTENSIONS = new Set(["mp4", "m4v", "mov", "webm", "mkv"]);
|
||||||
|
|
||||||
|
const MIME_EXTENSION_MAP: Record<string, string> = {
|
||||||
|
"image/jpeg": "jpg",
|
||||||
|
"image/jpg": "jpg",
|
||||||
|
"image/png": "png",
|
||||||
|
"image/webp": "webp",
|
||||||
|
"image/gif": "gif",
|
||||||
|
"image/bmp": "bmp",
|
||||||
|
"image/avif": "avif",
|
||||||
|
"image/heic": "heic",
|
||||||
|
"image/heif": "heif",
|
||||||
|
"image/svg+xml": "svg",
|
||||||
|
"video/mp4": "mp4",
|
||||||
|
"video/webm": "webm",
|
||||||
|
"video/quicktime": "mov",
|
||||||
|
"video/x-m4v": "m4v",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DOWNLOAD_USER_AGENT =
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
|
function normalizeContentType(raw: string | null): string {
|
||||||
|
return raw?.split(";")[0]?.trim().toLowerCase() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExtension(raw: string | undefined | null): string | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
const trimmed = raw.replace(/^\./, "").trim().toLowerCase();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
if (trimmed === "jpeg") return "jpg";
|
||||||
|
if (trimmed === "jpg") return "jpg";
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExtensionFromUrl(rawUrl: string): string | undefined {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(rawUrl);
|
||||||
|
const extFromPath = normalizeExtension(path.posix.extname(parsed.pathname));
|
||||||
|
if (extFromPath) return extFromPath;
|
||||||
|
const extFromFormat = normalizeExtension(parsed.searchParams.get("format"));
|
||||||
|
if (extFromFormat) return extFromFormat;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveKindFromContentType(contentType: string): MediaKind | undefined {
|
||||||
|
if (!contentType) return undefined;
|
||||||
|
if (contentType.startsWith("image/")) return "image";
|
||||||
|
if (contentType.startsWith("video/")) return "video";
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveKindFromExtension(ext: string | undefined): MediaKind | undefined {
|
||||||
|
if (!ext) return undefined;
|
||||||
|
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
||||||
|
if (VIDEO_EXTENSIONS.has(ext)) return "video";
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMediaKind(
|
||||||
|
rawUrl: string,
|
||||||
|
contentType: string,
|
||||||
|
extension: string | undefined,
|
||||||
|
hint: MediaHint
|
||||||
|
): MediaKind | undefined {
|
||||||
|
const kindFromType = resolveKindFromContentType(contentType);
|
||||||
|
if (kindFromType) return kindFromType;
|
||||||
|
|
||||||
|
const kindFromExtension = resolveKindFromExtension(extension);
|
||||||
|
if (kindFromExtension) return kindFromExtension;
|
||||||
|
|
||||||
|
if (contentType && contentType !== "application/octet-stream") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hint === "image" ? "image" : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOutputExtension(
|
||||||
|
contentType: string,
|
||||||
|
extension: string | undefined,
|
||||||
|
kind: MediaKind
|
||||||
|
): string {
|
||||||
|
const extFromMime = normalizeExtension(MIME_EXTENSION_MAP[contentType]);
|
||||||
|
if (extFromMime) return extFromMime;
|
||||||
|
|
||||||
|
const normalizedExt = normalizeExtension(extension);
|
||||||
|
if (normalizedExt) return normalizedExt;
|
||||||
|
|
||||||
|
return kind === "video" ? "mp4" : "jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeDecodeURIComponent(value: string): string {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFileSegment(input: string): string {
|
||||||
|
return input
|
||||||
|
.replace(/[^a-zA-Z0-9_-]+/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.replace(/^[-_]+|[-_]+$/g, "")
|
||||||
|
.slice(0, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFileStem(rawUrl: string, extension: string): string {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(rawUrl);
|
||||||
|
const base = path.posix.basename(parsed.pathname);
|
||||||
|
if (!base) return "";
|
||||||
|
const decodedBase = safeDecodeURIComponent(base);
|
||||||
|
const normalizedExt = normalizeExtension(extension);
|
||||||
|
const stripExt = normalizedExt ? new RegExp(`\\.${normalizedExt}$`, "i") : null;
|
||||||
|
const rawStem = stripExt ? decodedBase.replace(stripExt, "") : decodedBase;
|
||||||
|
return sanitizeFileSegment(rawStem);
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFileName(kind: MediaKind, index: number, sourceUrl: string, extension: string): string {
|
||||||
|
const stem = resolveFileStem(sourceUrl, extension);
|
||||||
|
const prefix = kind === "image" ? "img" : "video";
|
||||||
|
const serial = String(index).padStart(3, "0");
|
||||||
|
const suffix = stem ? `-${stem}` : "";
|
||||||
|
return `${prefix}-${serial}${suffix}.${extension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectMarkdownLinkCandidates(markdown: string): MarkdownLinkCandidate[] {
|
||||||
|
const candidates: MarkdownLinkCandidate[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
|
||||||
|
if (fmMatch) {
|
||||||
|
const coverMatch = fmMatch[1]?.match(FRONTMATTER_COVER_RE);
|
||||||
|
if (coverMatch?.[2] && !seen.has(coverMatch[2])) {
|
||||||
|
seen.add(coverMatch[2]);
|
||||||
|
candidates.push({ url: coverMatch[2], hint: "image" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MARKDOWN_LINK_RE.lastIndex = 0;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = MARKDOWN_LINK_RE.exec(markdown))) {
|
||||||
|
const label = match[1] ?? "";
|
||||||
|
const rawUrl = match[3] ?? "";
|
||||||
|
if (!rawUrl || seen.has(rawUrl)) continue;
|
||||||
|
seen.add(rawUrl);
|
||||||
|
candidates.push({
|
||||||
|
url: rawUrl,
|
||||||
|
hint: label.startsWith("![") ? "image" : "unknown",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteMarkdownMediaLinks(markdown: string, replacements: Map<string, string>): string {
|
||||||
|
if (replacements.size === 0) return markdown;
|
||||||
|
MARKDOWN_LINK_RE.lastIndex = 0;
|
||||||
|
|
||||||
|
let result = markdown.replace(MARKDOWN_LINK_RE, (full, label, _openAngle, rawUrl) => {
|
||||||
|
const localPath = replacements.get(rawUrl);
|
||||||
|
if (!localPath) return full;
|
||||||
|
return `${label}(${localPath})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
result = result.replace(FRONTMATTER_COVER_RE, (full, prefix, rawUrl, suffix) => {
|
||||||
|
const localPath = replacements.get(rawUrl);
|
||||||
|
if (!localPath) return full;
|
||||||
|
return `${prefix}${localPath}${suffix}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function localizeMarkdownMedia(
|
||||||
|
markdown: string,
|
||||||
|
options: LocalizeMarkdownMediaOptions
|
||||||
|
): Promise<LocalizeMarkdownMediaResult> {
|
||||||
|
const log = options.log ?? (() => {});
|
||||||
|
const markdownDir = path.dirname(options.markdownPath);
|
||||||
|
const candidates = collectMarkdownLinkCandidates(markdown);
|
||||||
|
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
return {
|
||||||
|
markdown,
|
||||||
|
downloadedImages: 0,
|
||||||
|
downloadedVideos: 0,
|
||||||
|
imageDir: null,
|
||||||
|
videoDir: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacements = new Map<string, string>();
|
||||||
|
let downloadedImages = 0;
|
||||||
|
let downloadedVideos = 0;
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(candidate.url, {
|
||||||
|
method: "GET",
|
||||||
|
redirect: "follow",
|
||||||
|
headers: {
|
||||||
|
"user-agent": DOWNLOAD_USER_AGENT,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
log(`[url-to-markdown] Skip media (${response.status}): ${candidate.url}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceUrl = response.url || candidate.url;
|
||||||
|
const contentType = normalizeContentType(response.headers.get("content-type"));
|
||||||
|
const extension = resolveExtensionFromUrl(sourceUrl) ?? resolveExtensionFromUrl(candidate.url);
|
||||||
|
const kind = resolveMediaKind(sourceUrl, contentType, extension, candidate.hint);
|
||||||
|
if (!kind) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputExtension = resolveOutputExtension(contentType, extension, kind);
|
||||||
|
const nextIndex = kind === "image" ? downloadedImages + 1 : downloadedVideos + 1;
|
||||||
|
const dirName = kind === "image" ? "imgs" : "videos";
|
||||||
|
const targetDir = path.join(markdownDir, dirName);
|
||||||
|
await mkdir(targetDir, { recursive: true });
|
||||||
|
|
||||||
|
const fileName = buildFileName(kind, nextIndex, sourceUrl, outputExtension);
|
||||||
|
const absolutePath = path.join(targetDir, fileName);
|
||||||
|
const relativePath = path.posix.join(dirName, fileName);
|
||||||
|
const bytes = Buffer.from(await response.arrayBuffer());
|
||||||
|
await writeFile(absolutePath, bytes);
|
||||||
|
replacements.set(candidate.url, relativePath);
|
||||||
|
|
||||||
|
if (kind === "image") {
|
||||||
|
downloadedImages = nextIndex;
|
||||||
|
} else {
|
||||||
|
downloadedVideos = nextIndex;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error ?? "");
|
||||||
|
log(`[url-to-markdown] Failed to download media ${candidate.url}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
markdown: rewriteMarkdownMediaLinks(markdown, replacements),
|
||||||
|
downloadedImages,
|
||||||
|
downloadedVideos,
|
||||||
|
imageDir: downloadedImages > 0 ? path.join(markdownDir, "imgs") : null,
|
||||||
|
videoDir: downloadedVideos > 0 ? path.join(markdownDir, "videos") : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countRemoteMedia(markdown: string): { images: number; videos: number; hasCoverImage: boolean } {
|
||||||
|
const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
|
||||||
|
const hasCoverImage = !!(fmMatch?.[1]?.match(FRONTMATTER_COVER_RE)?.[2]);
|
||||||
|
const candidates = collectMarkdownLinkCandidates(markdown);
|
||||||
|
let images = 0;
|
||||||
|
let videos = 0;
|
||||||
|
for (const c of candidates) {
|
||||||
|
const ext = resolveExtensionFromUrl(c.url);
|
||||||
|
const kind = resolveKindFromExtension(ext);
|
||||||
|
if (kind === "video") {
|
||||||
|
videos++;
|
||||||
|
} else if (kind === "image" || c.hint === "image") {
|
||||||
|
images++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { images, videos, hasCoverImage };
|
||||||
|
}
|
||||||
@@ -167,7 +167,7 @@ When generating multiple images in a series:
|
|||||||
1. **Image 1 (cover)**: Generate without `--ref` — this establishes the visual anchor
|
1. **Image 1 (cover)**: Generate without `--ref` — this establishes the visual anchor
|
||||||
2. **Images 2+**: Always pass image 1 as `--ref` to the image generation skill:
|
2. **Images 2+**: Always pass image 1 as `--ref` to the image generation skill:
|
||||||
```bash
|
```bash
|
||||||
npx -y bun ${SKILL_DIR}/scripts/main.ts \
|
${BUN_X} ${SKILL_DIR}/scripts/main.ts \
|
||||||
--promptfiles prompts/02-content-xxx.md \
|
--promptfiles prompts/02-content-xxx.md \
|
||||||
--ref path/to/01-cover-xxx.png \
|
--ref path/to/01-cover-xxx.png \
|
||||||
--image 02-content-xxx.png --ar 3:4 --quality 2k
|
--image 02-content-xxx.png --ar 3:4 --quality 2k
|
||||||
|
|||||||
Reference in New Issue
Block a user