Compare commits

...

161 Commits

Author SHA1 Message Date
Jim Liu 宝玉 46af919634 chore: release v1.53.0 2026-03-06 21:20:22 -06:00
Jim Liu 宝玉 5560db595a feat(baoyu-url-to-markdown): add HTML snapshot saving and Defuddle fallback pipeline
- Save rendered HTML as sibling -captured.html file alongside markdown
- Defuddle-first conversion with automatic fallback to legacy Readability/selector extractor
- Add rawHtml, conversionMethod, fallbackReason to ConversionResult
- Log converter method and fallback reason in CLI output
2026-03-06 21:18:21 -06:00
Jim Liu 宝玉 3b031c7768 chore: release v1.52.0 2026-03-06 20:54:11 -06:00
Jim Liu 宝玉 39c7e86a8d feat(baoyu-post-to-weibo): add video support and improve upload reliability
- Add --video flag for video uploads (max 18 files total)
- Switch from clipboard paste to DOM.setFileInputFiles for uploads
- Add Chrome health check with auto-restart for unresponsive instances
- Add navigation check to ensure Weibo home page before posting
2026-03-06 20:27:18 -06:00
Jim Liu 宝玉 f00ca11d9a chore: release v1.51.2 2026-03-06 17:21:40 -06:00
Jim Liu 宝玉 87c2509268 fix(baoyu-infographic): add credential stripping to address Snyk W007 audit 2026-03-06 17:21:10 -06:00
Jim Liu 宝玉 20bf6d9ad8 fix(release-skills): use generic changelog language patterns to avoid URL scanner false positive 2026-03-06 17:21:06 -06:00
Jim Liu 宝玉 33bc0a6255 chore: release v1.51.1 2026-03-06 16:05:25 -06:00
Jim Liu 宝玉 c15a44b439 fix(security): remove curl|bash, enforce HTTPS-only downloads
- Replace all curl|bash install suggestions with brew/npm
- downloadFile: HTTPS-only, reject http:// URLs
- downloadFile: add redirect limit (max 5)
- Remove unused http import from md-to-html scripts
- Add Security Guidelines section to CLAUDE.md
- Update SKILL.md profile dir references
2026-03-06 16:03:10 -06:00
Jim Liu 宝玉 6e533f938f refactor: unify Chrome CDP profile path across all skills
All skills now share a single Chrome profile at:
- macOS: ~/Library/Application Support/baoyu-skills/chrome-profile
- Linux: $XDG_DATA_HOME/baoyu-skills/chrome-profile
- Env override: BAOYU_CHROME_PROFILE_DIR

Fixes baoyu-post-to-weibo incorrectly reusing x-browser-profile.
Legacy per-skill env vars retained as fallback.
2026-03-06 16:03:01 -06:00
Jim Liu 宝玉 c60eb85629 docs: add baoyu-post-to-weibo to README (en/zh) 2026-03-06 14:59:37 -06:00
Jim Liu 宝玉 20d2a78f87 chore: release v1.51.0 2026-03-06 14:56:49 -06:00
Jim Liu 宝玉 bb63ee2a2e feat(baoyu-format-markdown): add title/summary multi-candidate selection with auto_select support 2026-03-06 14:56:32 -06:00
Jim Liu 宝玉 5fc697166d feat(baoyu-post-to-weibo): add Weibo posting skill with text, images, and headline articles 2026-03-06 14:56:28 -06:00
Jim Liu 宝玉 1ed204bd5a chore: release v1.50.0 2026-03-06 02:33:47 -06:00
Jim Liu 宝玉 be2cbecfb0 feat(baoyu-translate): expand translation style presets from 4 to 9 with CLI flag support
Add 5 new style presets (academic, business, humorous, conversational,
elegant) to existing options. Wire --style CLI flag, update subagent
prompt template with style section, and document in both READMEs.
2026-03-06 02:33:25 -06:00
Jim Liu 宝玉 bce96e411d chore: release v1.49.0 2026-03-06 01:55:40 -06:00
Jim Liu 宝玉 fe404c493d feat(baoyu-translate): extract workflow mechanics, expand triggers, and save frontmatter in chunks
- Extract Step 2 materialization details to references/workflow-mechanics.md
- Expand description trigger keywords (改成中文, 快翻, 本地化, etc.)
- Add proactive warning for long content in quick mode
- Save frontmatter to chunks/frontmatter.md in chunk.ts
- Fix step number references in upgrade prompt
- Simplify refined-workflow draft step to reference SKILL.md principles
2026-03-06 01:55:18 -06:00
Jim Liu 宝玉 6dd754ccdd feat(baoyu-format-markdown): add reader-perspective analysis phase and restructure workflow
- Add Step 2 content analysis from reader's perspective (highlights, structure, formatting issues)
- Output analysis to {filename}-analysis.md as formatting blueprint
- Restructure from 8 steps to 7 steps with clearer phase separation
- Add explicit do/don't formatting principles
- Add completion report template with change summary
2026-03-06 01:55:13 -06:00
Jim Liu 宝玉 1f4415c8ff chore: release v1.48.2 2026-03-06 00:01:19 -06:00
Jim Liu 宝玉 b25eb87bb4 feat(baoyu-translate): add figurative language & emotional fidelity checks to refined workflow and quick mode 2026-03-06 00:01:07 -06:00
Jim Liu 宝玉 dbf35503a3 chore: add posts/ to .gitignore 2026-03-06 00:01:04 -06:00
Jim Liu 宝玉 36ccbd0300 chore: release v1.48.1 2026-03-05 23:35:52 -06:00
Jim Liu 宝玉 bc878e5157 feat(baoyu-translate): add figurative language analysis and meaning-first translation principles 2026-03-05 23:35:08 -06:00
Jim Liu 宝玉 727375afa3 chore: release v1.48.0 2026-03-05 20:54:11 -06:00
Jim Liu 宝玉 f2c914887a feat(baoyu-translate): add --output-dir to chunk.ts and improve refined workflow
- chunk.ts: add --output-dir option so chunks write to output directory instead of source directory
- Refined workflow: split Review into Critical Review + Revision (5→6 steps)
- Add Europeanized language diagnosis for CJK targets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:53:54 -06:00
Jim Liu 宝玉 9f76a96741 chore: release v1.47.0 2026-03-05 19:35:07 -06:00
Jim Liu 宝玉 67e3e11cce feat(skills): add cross-platform PowerShell support for EXTEND.md checks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:32:35 -06:00
Jim Liu 宝玉 5b4ba3ac3f feat(baoyu-translate): add three-mode translation skill with glossary support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:32:27 -06:00
Jim Liu 宝玉 ce259e4547 chore: release v1.46.0 2026-03-05 14:34:35 -06:00
Jim Liu 宝玉 fff1a54b6b feat(baoyu-url-to-markdown): add --output-dir option for custom output directory
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:34:07 -06:00
Jim Liu 宝玉 9437581c48 chore: release v1.45.1 2026-03-05 10:31:54 -06:00
Jim Liu 宝玉 bd4db203f8 refactor(skills): replace hardcoded npx -y bun with ${BUN_X} runtime variable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:31:29 -06:00
Jim Liu 宝玉 51387498a5 refactor(project): add BUN_X runtime detection with bun-first fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:31:24 -06:00
Jim Liu 宝玉 6d1336d8bd chore: release v1.45.0 2026-03-05 00:55:59 -06:00
Jim Liu 宝玉 42931d6294 feat(baoyu-post-to-x): add post-composition verification and improve stability
Add automatic check after article images are inserted: verifies remaining
XIMGPH placeholders and expected vs actual image count. Increase CDP timeout
to 60s and add 3s DOM stabilization delay between image insertions.
2026-03-05 00:55:56 -06:00
Jim Liu 宝玉 4c9e57777b chore: release v1.44.0 2026-03-05 00:37:52 -06:00
Jim Liu 宝玉 210905ef66 feat(baoyu-url-to-markdown): add media download and cover image extraction
- Add --download-media flag to download images/videos to local dirs and rewrite markdown links
- Extract coverImage from page meta (og:image) into YAML front matter
- Handle data-src lazy loading for WeChat and similar sites
- Add EXTEND.md preferences with first-time setup for download_media setting
- Add media-localizer module adapted from x-to-markdown
2026-03-05 00:37:09 -06:00
Jim Liu 宝玉 bd4de7b995 chore: release v1.43.2 2026-03-05 00:12:44 -06:00
Jim Liu 宝玉 832f06e86e refactor(baoyu-url-to-markdown): replace custom extraction with defuddle library 2026-03-05 00:11:59 -06:00
Jim Liu 宝玉 a7ba3d73db chore: release v1.43.1 2026-03-02 12:07:42 -06:00
Jim Liu 宝玉 e2a15aadcc feat(baoyu-danger-x-to-markdown): WSL auto-detection and debug port env var
Add X_DEBUG_PORT env var for fixed port in WSL.
Auto-detect WSL and resolve Chrome profile to Windows-native path.
2026-03-02 12:07:16 -06:00
Jim Liu 宝玉 e3f00c103e feat(baoyu-danger-gemini-web): WSL auto-detection and debug port env var
Add GEMINI_WEB_DEBUG_PORT env var for fixed port in WSL.
Auto-detect WSL and resolve Chrome profile to Windows-native path.
2026-03-02 12:07:12 -06:00
Jim Liu 宝玉 a501202ab6 feat(baoyu-post-to-wechat): auto-detect WSL for Chrome profile path
In WSL2, auto-resolve Windows USERPROFILE via cmd.exe/wslpath
so Chrome profile is stored on Windows-native filesystem.
2026-03-02 12:07:07 -06:00
Jim Liu 宝玉 166cb323dd feat(baoyu-post-to-x): auto-detect WSL for Chrome profile path
In WSL2, auto-resolve Windows USERPROFILE via cmd.exe/wslpath
so Chrome profile is stored on Windows-native filesystem.
Fixes login state persistence when using Windows Chrome from WSL.
2026-03-02 12:07:02 -06:00
Jim Liu 宝玉 b7585ebf71 chore: release v1.43.0 2026-03-02 11:53:22 -06:00
Jim Liu 宝玉 3205239067 feat(baoyu-post-to-x): support env var overrides for debug port and profile directory 2026-03-02 11:52:49 -06:00
Jim Liu 宝玉 c5b3066962 feat(baoyu-post-to-wechat): support env var overrides for debug port and profile directory 2026-03-02 11:52:45 -06:00
Jim Liu 宝玉 84d7777246 chore: release v1.42.3 2026-03-02 00:09:33 -06:00
Jim Liu 宝玉 8412392788 fix(baoyu-image-gen): use standard size presets for DashScope aspect ratio mapping
Replace free-form dimension calculation with closest-match from predefined
standard sizes to avoid invalid resolution errors from the API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 00:08:30 -06:00
Jim Liu 宝玉 7cc2e640b0 chore: release v1.42.2 2026-03-01 00:09:10 -06:00
Jim Liu 宝玉 e2fa3065f7 chore: add sync-md-to-wechat utility script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 00:01:09 -06:00
Jim Liu 宝玉 8787cbe85b feat(baoyu-post-to-wechat): internalize markdown conversion with modular renderer and color support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 00:01:05 -06:00
Jim Liu 宝玉 226d501e9e feat(baoyu-markdown-to-html): inline rendering pipeline and enhance modern theme
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 00:00:57 -06:00
Jim Liu 宝玉 6796ec67bd chore: release v1.42.1 2026-02-28 12:14:15 -06:00
Jim Liu 宝玉 856e8e5205 feat(baoyu-markdown-to-html): modularize render.ts and bundle code themes locally
Split monolithic render.ts into cli, constants, extend-config, html-builder, renderer, themes, and types modules. Bundle highlight.js code theme CSS files locally instead of CDN loading.
2026-02-28 12:13:39 -06:00
Jim Liu 宝玉 a7d6b5c69b chore: release v1.42.0 2026-02-28 11:51:46 -06:00
Jim Liu 宝玉 6e18725906 feat(baoyu-post-to-wechat): add default color preference and modern theme support 2026-02-28 11:48:59 -06:00
Jim Liu 宝玉 7133d54f04 feat(baoyu-markdown-to-html): consolidate heritage and warm into modern theme with per-theme color defaults 2026-02-28 11:48:55 -06:00
Jim Liu 宝玉 208bb20e98 chore: release v1.41.0 2026-02-28 11:25:22 -06:00
Jim Liu 宝玉 6edc1b7822 feat(baoyu-markdown-to-html): rename themes and add color presets with theme defaults
Rename red→heritage and orange→warm themes. Add 13 named color presets,
serif-cjk font family, and per-theme style defaults with CLI override support.
2026-02-28 11:18:16 -06:00
Jim Liu 宝玉 e754eae5ae chore: release v1.40.1 2026-02-28 01:38:40 -06:00
Jim Liu 宝玉 cc016f3b16 feat(baoyu-image-gen): clarify model resolution priority and add model display requirement
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 01:38:10 -06:00
Jim Liu 宝玉 2188160418 chore: release v1.40.0 2026-02-28 01:15:53 -06:00
Jim Liu 宝玉 77204d2e4a feat(baoyu-markdown-to-html): add CLI customization options and EXTEND.md config support
Add --color, --font-family, --font-size, --code-theme, --mac-code-block,
--line-number, --cite, --count, --legend options. Support EXTEND.md for
default preferences. Add code highlight theme fetching and mermaid fix.
2026-02-28 01:15:16 -06:00
Jim Liu 宝玉 35eeb62f10 Merge pull request #57 from zhao-newname/main
feat(baoyu-image-gen): support OpenAI chat completions endpoint for i…
2026-02-28 01:13:19 -06:00
jeff.zhao 7dfa2b6e6c feat(baoyu-image-gen): support OpenAI chat completions endpoint for image generation
Add OPENAI_IMAGE_USE_CHAT env var (true|false) to route image generation
through /chat/completions instead of /images/generations. Extracts base64
image data URLs from the chat response, enabling compatibility with
OpenAI-compatible providers that return images via chat API.
2026-02-28 14:46:05 +08:00
Jim Liu 宝玉 a2eecec26f chore: release v1.39.0 2026-02-28 00:33:53 -06:00
Jim Liu 宝玉 903e7ad79d feat(baoyu-markdown-to-html): add red and orange themes 2026-02-28 00:33:28 -06:00
Jim Liu 宝玉 13707fa2cf chore: release v1.38.0 2026-02-28 00:07:01 -06:00
Jim Liu 宝玉 b56e503b16 feat(baoyu-danger-x-to-markdown): add referenced tweet rendering, media reuse, and hi-res downloads
- Render embedded tweets in articles as blockquotes with author info and text summary
- Reuse existing markdown when --download-media targets already-converted URLs
- Upgrade Twitter image downloads to 4096x4096 resolution
- Improve entity resolution with logical key lookup
- Support trailing media for all block types (headings, lists, blockquotes)
- Change output path to {username}/{tweet-id}/{content-slug}.md for stable paths
2026-02-28 00:06:43 -06:00
Jim Liu 宝玉 7d03685ade chore: release v1.37.1 2026-02-27 18:35:25 -06:00
Jim Liu 宝玉 b305c386bc chore: remove unused Gemini model comparison images
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:32:05 -06:00
Jim Liu 宝玉 240dd7d314 Merge pull request #56 from xkcoding/fix/update-gemini-web-model-headers
fix(baoyu-danger-gemini-web): sync model headers with upstream and update model list
2026-02-27 18:30:48 -06:00
Jim Liu 宝玉 b02ceacfd9 chore: release v1.37.0 2026-02-27 18:29:04 -06:00
Jim Liu 宝玉 fdf9007e2c feat(baoyu-danger-x-to-markdown): improve article rendering with inline links and content-based output paths 2026-02-27 18:28:34 -06:00
Jim Liu 宝玉 08cee885d3 chore: release v1.36.0 2026-02-27 10:13:27 -06:00
Jim Liu 宝玉 3bd5fdeb1b feat(baoyu-image-gen): add gemini-3.1-flash-image-preview model and improve first-time setup
- Add gemini-3.1-flash-image-preview to supported Google multimodal models
- Improve preferences loading with blocking first-time setup flow
- Add first-time-setup.md reference for guided configuration
- Update model references in SKILL.md and preferences schema
2026-02-27 10:12:52 -06:00
Yangkai.Shen e737c4a611 docs: add model comparison images for PR #56
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:59:42 +08:00
Yangkai.Shen 2eec4f3639 fix(baoyu-danger-gemini-web): sync model headers with upstream and update model list
- Update x-goog-ext-525001261-jspb header format to match upstream
  HanaokaYuzu/Gemini-API (commit 42900f7, 2026-02-03), appending
  `,null,null,1` to fix image generation failures
- Replace deprecated gemini-2.5-pro and gemini-2.5-flash with
  gemini-3.0-flash and gemini-3.0-flash-thinking
- Add gemini-3.1-pro-preview (empty header, server auto-routed)
- Update SKILL.md and CLI help text to reflect new model options

Closes #50

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:40:50 +08:00
Jim Liu 宝玉 e5912018f3 Merge pull request #55 from liye71023326/fix/google-proxy-support
fix(baoyu-image-gen): use curl fallback when HTTP proxy is detected
2026-02-26 14:33:55 -06:00
李野 b1f568d03d fix(baoyu-image-gen): use curl fallback for Google API when HTTP proxy is detected
Bun's fetch implementation has a known issue where long-lived connections
through HTTP proxies (e.g., Clash, V2Ray) get their sockets closed
unexpectedly, causing Google image generation requests to fail with
"The socket connection was closed unexpectedly".

This change adds automatic proxy detection and falls back to curl as the
HTTP client when a proxy is configured (via https_proxy, http_proxy,
HTTPS_PROXY, HTTP_PROXY, or ALL_PROXY environment variables). When no
proxy is detected, the original fetch-based implementation is used.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:16:15 +08:00
Jim Liu 宝玉 e736707628 chore: release v1.35.0 2026-02-24 22:15:29 -06:00
Jim Liu 宝玉 d863f11f61 feat(baoyu-infographic): add dense-modules layout and 3 new styles for high-density infographics
Add dense-modules layout for data-rich guides and 3 new styles:
morandi-journal, pop-laboratory, retro-pop-grid. Add keyword shortcuts
for 高密度信息大图 auto-selection.

Prompt credit: AJ (https://waytoagi.feishu.cn/wiki/YG0zwalijihRREkgmPzcWRInnUg)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 22:13:47 -06:00
Jim Liu 宝玉 2ce873c65c Merge pull request #47 from justnode/feature/add-baoyu-image-gen-provider
Feature/add baoyu image gen provider
2026-02-24 21:53:19 -06:00
Jim Liu 宝玉 964cf1e045 chore: release v1.34.2 2026-02-24 18:51:36 -06:00
Jim Liu 宝玉 eded9a98bb docs(baoyu-post-to-wechat): enforce explicit theme passing for markdown conversion 2026-02-24 18:51:10 -06:00
Jim Liu 宝玉 a64fdbd23f docs(baoyu-markdown-to-html): clarify theme resolution with EXTEND.md fallbacks 2026-02-24 18:51:04 -06:00
justnodejs 36b9c5e197 docs(baoyu-image-gen): add replicate model configuration documentation 2026-02-24 23:25:38 +08:00
justnodejs 851497abbd refactor(baoyu-image-gen): update replicate default model to nano-banana-pro 2026-02-24 20:26:47 +08:00
justnodejs 65a561e654 feat(baoyu-image-gen): add replicate provider 2026-02-24 19:12:36 +08:00
Jim Liu 宝玉 7b2c02a007 chore: release v1.34.1 2026-02-20 03:00:26 -06:00
Jim Liu 宝玉 98f49eae96 Merge pull request #45 from LyInfi/fix/wechat-browser-upload-progress-reeval
fix(wechat-browser): fix upload progress check crashing on second iteration
2026-02-19 18:41:51 -06:00
LyInfi 1bdf44df9e fix(wechat-browser): fix upload progress check crashing on second iteration
Runtime.evaluate reuses the same JS execution context across calls in a
session. The previous expression used `const thumbs = ...` which throws
"Identifier 'thumbs' has already been declared" on the second loop
iteration, causing result.value to be undefined and JSON.parse to throw
"JSON Parse error: Unexpected identifier 'undefined'".

Fix by inlining the querySelector into a single expression with no
variable declaration, eliminating the re-declaration error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 12:12:26 +08:00
Jim Liu 宝玉 d59ecf22c1 chore: release v1.34.0 2026-02-17 13:32:50 -06:00
Jim Liu 宝玉 22fa1a62fc refactor(baoyu-article-illustrator): enforce prompt file creation before image generation 2026-02-17 13:30:34 -06:00
Jim Liu 宝玉 3d48eed33a Merge pull request #44 from jeffrey94/feat/ref-image-chain
feat(baoyu-xhs-images): add reference image chain for visual consistency
2026-02-17 13:19:34 -06:00
Jim Liu 宝玉 8f1c4a65dd chore: release v1.33.1 2026-02-14 14:52:51 -06:00
Jim Liu 宝玉 9b97720f16 docs(baoyu-post-to-x): remove --submit flag and clarify manual publish workflow 2026-02-14 14:48:18 -06:00
Jim Liu 宝玉 145e1d2d04 refactor(baoyu-post-to-x): replace hand-rolled markdown parser with marked ecosystem
Switch md-to-html from manual line-by-line parsing to marked + front-matter +
highlight.js + remark-cjk-friendly for robust markdown conversion with syntax
highlighting, proper CJK handling, and standard frontmatter parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 14:37:14 -06:00
jeffrey94 49403b6fab feat(baoyu-xhs-images): add reference image chain for visual consistency
When generating multi-image series, use image 1 as --ref for all
subsequent images. This anchors character design, color rendering,
and illustration style across the entire series — critical for styles
with recurring characters or mascots.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 00:49:09 +08:00
Jim Liu 宝玉 6d56f1dae8 chore: release v1.33.0 2026-02-13 20:28:11 -06:00
Jim Liu 宝玉 dd08a2aa89 feat(baoyu-post-to-wechat): add pre-flight environment check script 2026-02-13 20:27:31 -06:00
Jim Liu 宝玉 d9b47debb3 feat(baoyu-post-to-x): add pre-flight check and image upload verification 2026-02-13 20:27:27 -06:00
Jim Liu 宝玉 6cc8627ca8 chore: release v1.32.0 2026-02-12 01:58:23 -06:00
Jim Liu 宝玉 54103dfd7d refactor(baoyu-post-to-wechat): prioritize coverImage over featureImage in frontmatter lookup 2026-02-12 01:56:32 -06:00
Jim Liu 宝玉 6012b9cc88 refactor(baoyu-format-markdown): rename featureImage to coverImage as primary frontmatter key 2026-02-12 01:56:28 -06:00
Jim Liu 宝玉 260b71cdd7 feat(baoyu-danger-x-to-markdown): add --download-media flag and media localization 2026-02-12 01:56:24 -06:00
Jim Liu 宝玉 9ff468a6a7 chore: release v1.31.2 2026-02-10 16:05:48 -06:00
Jim Liu 宝玉 c83d114a73 fix(baoyu-post-to-x): fix Windows clipboard and path handling
- Embed file path directly in PowerShell script with single-quote escaping
  instead of using param()/−Path which fails with -Command parameter.
- Use fileURLToPath() for getScriptDir() to handle Windows drive-letter paths.
2026-02-10 16:05:09 -06:00
Jim Liu 宝玉 e964100dd9 fix(baoyu-post-to-wechat): fix PowerShell clipboard copy on Windows
Embed file path directly in PowerShell script with single-quote escaping
instead of using param()/−Path which fails with -Command parameter.
2026-02-10 16:05:03 -06:00
Jim Liu 宝玉 6c0ae7a86a chore: release v1.31.1 2026-02-10 15:50:31 -06:00
Jim Liu 宝玉 52ade637af fix(baoyu-post-to-x): fix Chrome launch on macOS and cover image path resolution
Use open -na on macOS to avoid process blocking; resolve relative cover image
paths to absolute paths.
2026-02-10 15:46:14 -06:00
Jim Liu 宝玉 569beebdd6 feat(baoyu-post-to-wechat): adapt to new WeChat UI and fix digest/cover handling
Rename 图文 to 贴图 throughout; add ProseMirror editor support with old editor
fallback; add fallback file input selector; add upload progress monitoring;
improve save button detection with toast verification; truncate digest > 120
chars at punctuation boundary; fix cover image relative path resolution.
2026-02-10 15:46:09 -06:00
Jim Liu 宝玉 6cbf0f4e52 chore: release v1.31.0 2026-02-07 15:51:09 -06:00
Jim Liu 宝玉 5615e6150f feat(baoyu-post-to-wechat): add comment control, cover fallback chain, and first-time setup 2026-02-07 15:50:23 -06:00
Jim Liu 宝玉 7465f37dcf chore: release v1.30.3 2026-02-06 16:22:02 -06:00
Jim Liu 宝玉 38a1b90db3 refactor(baoyu-article-illustrator): optimize SKILL.md from 197 to 150 lines 2026-02-06 16:21:58 -06:00
Jim Liu 宝玉 b53a5db60a chore: release v1.30.2 2026-02-06 16:13:51 -06:00
Jim Liu 宝玉 372e03cef3 refactor(baoyu-cover-image): optimize SKILL.md from 532 to 233 lines 2026-02-06 16:13:47 -06:00
Jim Liu 宝玉 ea8ad82786 chore: release v1.30.1 2026-02-06 16:07:34 -06:00
Jim Liu 宝玉 6fffe252c0 fix(baoyu-cover-image): enhance reference image analysis and prompt generation
- Add deep analysis template for extracting specific visual elements
- Require MUST INCORPORATE section with concrete, reproducible details
- Add integration approach for spatial layout instructions
- Clarify ref-capable backend requirements (Google/OpenAI)
2026-02-06 16:07:09 -06:00
Jim Liu 宝玉 86a84739e8 feat(baoyu-image-gen): add OpenAI GPT Image edits support for reference images
- Support --ref with OpenAI GPT Image models (gpt-image-1.5)
- Auto-select Google or OpenAI when --ref provided
- Change ref-related warnings to explicit errors with fix hints
- Add reference image validation before generation
- Improve retry logic to skip non-retryable errors
2026-02-06 16:06:58 -06:00
Jim Liu 宝玉 7f80100b3e chore: release v1.30.0 2026-02-06 14:56:00 -06:00
Jim Liu 宝玉 61960961ba feat(baoyu-cover-image): add font dimension with 4 styles
Add 6th dimension for typography control:
- clean: modern geometric sans-serif
- handwritten: warm hand-lettered style
- serif: classic elegant typography
- display: bold decorative headlines

Includes auto-selection rules, compatibility matrix, and warm-flat preset.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 14:55:11 -06:00
Jim Liu 宝玉 cdd9ee042d chore: release v1.29.0 2026-02-06 13:36:08 -06:00
Jim Liu 宝玉 c742bfa1af fix(baoyu-url-to-markdown): improve html extraction and markdown conversion 2026-02-06 13:35:28 -06:00
Jim Liu 宝玉 0faea4ecaa Merge pull request #38 from kingdomad/feature/baoyu-image-gen-extend-config
add EXTEND.md configuration support
2026-02-05 23:10:11 -06:00
Jim Liu 宝玉 7c9ec259a1 Merge pull request #36 from NantesCheval/fix/wechat-title-and-list-duplication
fix(baoyu-post-to-wechat): fix title and list number duplication in WeChat articles
2026-02-05 23:09:17 -06:00
史提芬达 24a17709a8 add EXTEND.md configuration support 2026-02-05 19:46:22 +08:00
AlexCheval a3849af0cf fix(baoyu-post-to-wechat): fix title and list number duplication in WeChat articles
- Remove title from body content when extracting it for WeChat title field
  to prevent duplicate title display (one in header, one in content)
- Remove manual ordered list prefix since HTML <ol> already provides numbering,
  preventing "1.1.", "2.2.", "3.3." duplication

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 10:04:34 +08:00
Jim Liu 宝玉 cc95e6fe05 chore: release v1.28.4 2026-02-03 11:49:41 -06:00
Jim Liu 宝玉 e7d9ed7917 fix(baoyu-post-to-wechat): remove extra empty lines after image paste and fix summary timing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 11:49:13 -06:00
Jim Liu 宝玉 876c6332f6 feat(baoyu-markdown-to-html): add HTML meta tags and quote stripping for frontmatter
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 11:49:07 -06:00
Jim Liu 宝玉 502d2448b2 chore: release v1.28.3 2026-02-03 11:06:59 -06:00
Jim Liu 宝玉 b9e48d9483 fix(baoyu-post-to-wechat): fix placeholder matching to avoid WECHATIMGPH_1 matching WECHATIMGPH_10 2026-02-03 11:06:41 -06:00
Jim Liu 宝玉 b0554f8d0c chore: release v1.28.2 2026-02-03 11:01:25 -06:00
Jim Liu 宝玉 12f3d95639 fix(baoyu-post-to-x): improve Chrome reuse and placeholder matching in x-article 2026-02-03 11:01:09 -06:00
Jim Liu 宝玉 809fb29ca9 chore: release v1.28.1 2026-02-02 09:11:49 -06:00
Jim Liu 宝玉 be20bdf0be refactor(baoyu-article-illustrator): simplify SKILL.md and add Core Styles tier
- Extract detailed workflow to references/workflow.md
- Add Core Styles for quick selection (vector, minimal-flat, sci-fi, hand-drawn, editorial, scene)
- Add vector-illustration as recommended default style
- Add Illustration Purpose (information/visualization/imagination)
- Add default composition, character rendering, text styling to prompts
2026-02-02 09:11:43 -06:00
Jim Liu 宝玉 7c36b2a6a5 chore: release v1.28.0 2026-02-01 19:49:49 -06:00
Jim Liu 宝玉 7979cb9dde chore: ignore baoyu-skill-evolution directory 2026-02-01 19:49:22 -06:00
Jim Liu 宝玉 e563bbae7b feat(baoyu-post-to-wechat): add newspic article type support
- Add ArticleOptions interface with news/newspic types
- Track all image media IDs for newspic format
- Restructure publishToDraft for dual article type handling
2026-02-01 19:49:14 -06:00
Jim Liu 宝玉 c63d1eec69 refactor(baoyu-xhs-images): enforce blocking first-time setup
- Mark preferences loading as BLOCKING operation
- Update workflow flow diagram and checklist
2026-02-01 19:49:05 -06:00
Jim Liu 宝玉 78d6d23359 refactor(baoyu-comic): enforce blocking first-time setup
- Mark preferences loading as BLOCKING operation
- Update workflow flow diagram
2026-02-01 19:48:56 -06:00
Jim Liu 宝玉 ec6781a231 feat(baoyu-article-illustrator): add reference image support
- Add reference image handling with direct/style/palette usage types
- Enhance first-time setup as blocking operation
- Update prompt construction with reference frontmatter format
2026-02-01 19:48:46 -06:00
Jim Liu 宝玉 edf9030675 feat(baoyu-cover-image): add reference image support and visual elements library
- Add --ref parameter for reference images (style/palette/direct usage)
- Add visual-elements.md with icon vocabulary by topic
- Enhance first-time setup as blocking operation
- Remove character limits from titles, use original source titles
- Update prompt template with reference handling
2026-02-01 19:48:37 -06:00
Jim Liu 宝玉 10e1c426a0 chore: release v1.27.0 2026-02-01 02:15:08 -06:00
Jim Liu 宝玉 dbab83ff82 feat(baoyu-markdown-to-html): add remark-cjk-friendly for CJK emphasis
Preprocess markdown with remark-cjk-friendly to properly handle
CJK emphasis markers before HTML conversion.
2026-02-01 02:14:12 -06:00
Jim Liu 宝玉 478e66a93a docs(baoyu-infographic): clarify AskUserQuestion usage
Emphasize combining multiple questions into single call.
2026-02-01 02:14:06 -06:00
Jim Liu 宝玉 3a5866eb4b refactor(baoyu-format-markdown): use remark-cjk-friendly for CJK emphasis
Replace custom CJK emphasis handling with remark-cjk-friendly library,
significantly simplifying the codebase.
2026-02-01 02:13:58 -06:00
Jim Liu 宝玉 c0162bb3af chore: release v1.26.1 2026-01-29 22:02:42 -06:00
Jim Liu 宝玉 e9342a16f8 fix(baoyu-xhs-images): remove notebook style, add backup rules for prompts and images 2026-01-29 22:01:47 -06:00
Jim Liu 宝玉 5cb86d8e51 feat(baoyu-slide-deck): add backup rules for source, prompt, and slide image files 2026-01-29 22:01:42 -06:00
Jim Liu 宝玉 66ed08f010 feat(baoyu-infographic): add backup rules for source, analysis, prompt, and image files 2026-01-29 22:01:22 -06:00
Jim Liu 宝玉 b179166d28 feat(baoyu-cover-image): add backup rules for source and prompt files 2026-01-29 22:01:17 -06:00
Jim Liu 宝玉 4805526649 feat(baoyu-comic): add backup rules for source, character sheet, prompts, and page images 2026-01-29 22:00:59 -06:00
Jim Liu 宝玉 89159bf86e feat(baoyu-article-illustrator): add backup rules for source, prompt, and image files 2026-01-29 22:00:37 -06:00
Jim Liu 宝玉 b45b5d2c6e chore: release v1.26.0 2026-01-29 20:59:45 -06:00
Jim Liu 宝玉 62866426c5 feat(baoyu-xhs-images): add notebook and study-notes styles with mindmap and quadrant layouts
- notebook: hand-drawn infographic style with watercolor rendering and Morandi palette
- study-notes: realistic handwritten photo style with blue pen, red annotations, yellow highlighter
- mindmap: center radial layout (4-8 branches)
- quadrant: four-quadrant / circular section layout
2026-01-29 20:59:24 -06:00
Jim Liu 宝玉 28a7db6129 chore: release v1.25.4 2026-01-29 17:17:58 -06:00
293 changed files with 21036 additions and 3597 deletions
+4 -2
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.25.3"
"version": "1.53.0"
},
"plugins": [
{
@@ -18,6 +18,7 @@
"./skills/baoyu-xhs-images",
"./skills/baoyu-post-to-x",
"./skills/baoyu-post-to-wechat",
"./skills/baoyu-post-to-weibo",
"./skills/baoyu-article-illustrator",
"./skills/baoyu-cover-image",
"./skills/baoyu-slide-deck",
@@ -45,7 +46,8 @@
"./skills/baoyu-compress-image",
"./skills/baoyu-url-to-markdown",
"./skills/baoyu-format-markdown",
"./skills/baoyu-markdown-to-html"
"./skills/baoyu-markdown-to-html",
"./skills/baoyu-translate"
]
}
]
+10 -10
View File
@@ -50,16 +50,16 @@ Just run `/release-skills` - auto-detects your project configuration.
**Language Detection Rules**:
| Filename Pattern | Language |
|------------------|----------|
| `CHANGELOG.md` (no suffix) | en (default) |
| `CHANGELOG.zh.md` / `CHANGELOG_CN.md` / `CHANGELOG.zh-CN.md` | zh |
| `CHANGELOG.ja.md` / `CHANGELOG_JP.md` | ja |
| `CHANGELOG.ko.md` / `CHANGELOG_KR.md` | ko |
| `CHANGELOG.de.md` / `CHANGELOG_DE.md` | de |
| `CHANGELOG.fr.md` / `CHANGELOG_FR.md` | fr |
| `CHANGELOG.es.md` / `CHANGELOG_ES.md` | es |
| `CHANGELOG.{lang}.md` | Corresponding language code |
Changelog files follow the pattern `CHANGELOG_{LANG}.md` or `CHANGELOG.{lang}.md`, where `{lang}` / `{LANG}` is a language or region code.
| Pattern | Example | Language |
|---------|---------|----------|
| No suffix | `CHANGELOG.md` | en (default) |
| `_{LANG}` (uppercase) | `CHANGELOG_CN.md`, `CHANGELOG_JP.md` | Corresponding language |
| `.{lang}` (lowercase) | `CHANGELOG.zh.md`, `CHANGELOG.ja.md` | Corresponding language |
| `.{lang-region}` | `CHANGELOG.zh-CN.md` | Corresponding region variant |
Common language codes: `zh` (Chinese), `ja` (Japanese), `ko` (Korean), `de` (German), `fr` (French), `es` (Spanish).
**Output Example**:
```
+5 -1
View File
@@ -152,8 +152,12 @@ slide-deck/
infographic/
illustrations/
comic/
translate/
posts/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
*.ipr
.claude/skills/baoyu-skill-evolution
+361
View File
@@ -2,6 +2,367 @@
English | [中文](./CHANGELOG.zh.md)
## 1.53.0 - 2026-03-06
### Features
- `baoyu-url-to-markdown`: save rendered HTML snapshot as `-captured.html` alongside markdown output
- `baoyu-url-to-markdown`: Defuddle-first markdown conversion with automatic fallback to legacy Readability/selector extractor
## 1.52.0 - 2026-03-06
### Features
- `baoyu-post-to-weibo`: add video upload support via `--video` flag (max 18 files total)
- `baoyu-post-to-weibo`: switch from clipboard paste to `DOM.setFileInputFiles` for more reliable uploads
### Fixes
- `baoyu-post-to-weibo`: add Chrome health check with auto-restart for unresponsive instances
- `baoyu-post-to-weibo`: add navigation check to ensure Weibo home page before posting
## 1.51.2 - 2026-03-06
### Fixes
- `release-skills`: replace explicit language filename patterns (e.g. `CHANGELOG.de.md`) with generic pattern to avoid Gen Agent Trust Hub URL scanner false positive
- `baoyu-infographic`: add credential/secret stripping instructions to address Snyk W007 insecure credential handling audit
## 1.51.1 - 2026-03-06
### Refactor
- Unify Chrome CDP profile path — all skills now share `baoyu-skills/chrome-profile` instead of per-skill directories
- Fix `baoyu-post-to-weibo` incorrectly reusing `x-browser-profile` path
### Fixes
- Remove `curl | bash` remote code execution pattern from all install instructions
- Enforce HTTPS-only for remote image downloads in `md-to-html` scripts
- Add redirect limit (max 5) to prevent infinite redirect loops
- Add Security Guidelines section to CLAUDE.md
## 1.51.0 - 2026-03-06
### Features
- `baoyu-post-to-weibo`: new skill for posting to Weibo — supports text posts with images and headline articles (头条文章) via Chrome CDP
- `baoyu-format-markdown`: add title/summary multi-candidate selection — generates 3 candidates for user to pick, with `auto_select` EXTEND.md support
## 1.50.0 - 2026-03-06
### Features
- `baoyu-translate`: expand translation style presets from 4 to 9 — add academic, business, humorous, conversational, and elegant styles
- `baoyu-translate`: add `--style` CLI flag for per-invocation style override
- `baoyu-translate`: integrate style instructions into subagent prompt template
## 1.49.0 - 2026-03-06
### Features
- `baoyu-format-markdown`: add reader-perspective content analysis phase — analyzes highlights, structure, and formatting issues before applying formatting
- `baoyu-format-markdown`: restructure workflow from 8 steps to 7 with explicit do/don't formatting principles and completion report
- `baoyu-translate`: extract Step 2 workflow mechanics to separate reference file for cleaner SKILL.md
- `baoyu-translate`: expand trigger keywords (改成中文, 快翻, 本地化, etc.) for better skill activation
- `baoyu-translate`: add proactive warning for long content in quick mode
- `baoyu-translate`: save frontmatter to `chunks/frontmatter.md` during chunking
## 1.48.2 - 2026-03-06
### Features
- `baoyu-translate`: add figurative language & emotional fidelity review steps to refined workflow critique and revision stages
- `baoyu-translate`: enhance quick mode to enforce meaning-first translation principles for figurative language
## 1.48.1 - 2026-03-05
### Features
- `baoyu-translate`: add figurative language & metaphor mapping to analysis step — interprets metaphors, idioms, and implied meanings before translation instead of translating literally
- `baoyu-translate`: add "meaning over words", "figurative language", and "emotional fidelity" translation principles to SKILL.md, refined workflow, and subagent prompt template
## 1.48.0 - 2026-03-05
### Features
- `baoyu-translate`: add `--output-dir` option to chunk.ts — chunks now write to the translation output directory instead of the source file directory
- `baoyu-translate`: improve refined workflow — split Review into Critical Review + Revision (5→6 steps), add Europeanized language diagnosis for CJK targets
## 1.47.0 - 2026-03-05
### Features
- Add `baoyu-translate` skill — three-mode translation (quick/normal/refined) with custom glossaries, audience-aware translation, and parallel chunked translation for long documents
- Add cross-platform PowerShell support for EXTEND.md preference checks across all skills
## 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
### Refactor
- `baoyu-url-to-markdown`: replace custom HTML extraction (linkedom + Readability + Turndown) with defuddle library for cleaner content extraction and markdown conversion
## 1.43.1 - 2026-03-02
### Features
- `baoyu-post-to-x`: auto-detect WSL environment and resolve Chrome profile to Windows-native path for stable login persistence
- `baoyu-post-to-wechat`: auto-detect WSL environment and resolve Chrome profile to Windows-native path for stable login persistence
- `baoyu-danger-gemini-web`: WSL auto-detection for Chrome profile path; add `GEMINI_WEB_DEBUG_PORT` env var for fixed debug port
- `baoyu-danger-x-to-markdown`: WSL auto-detection for Chrome profile path; add `X_DEBUG_PORT` env var for fixed debug port
## 1.43.0 - 2026-03-02
### Features
- `baoyu-post-to-wechat`: support env var overrides for browser debug port (`WECHAT_BROWSER_DEBUG_PORT`) and profile directory (`WECHAT_BROWSER_PROFILE_DIR`)
- `baoyu-post-to-x`: support env var overrides for browser debug port (`X_BROWSER_DEBUG_PORT`) and profile directory (`X_BROWSER_PROFILE_DIR`)
## 1.42.3 - 2026-03-02
### Fixes
- `baoyu-image-gen`: use standard size presets for DashScope aspect ratio mapping instead of free-form calculation
## 1.42.2 - 2026-03-01
### Features
- `baoyu-markdown-to-html`: inline rendering pipeline (no subprocess), fix CJK emphasis order, enhance modern theme with GFM alerts and improved typography
- `baoyu-post-to-wechat`: internalize markdown conversion with modular renderer, add color support, simplify publishing workflow
## 1.42.1 - 2026-02-28
### Features
- `baoyu-markdown-to-html`: modularize render.ts into cli, constants, extend-config, html-builder, renderer, themes, and types modules; bundle code highlighting themes locally
## 1.42.0 - 2026-02-28
### Features
- `baoyu-markdown-to-html`: consolidate heritage and warm into single modern theme, add per-theme color defaults (default→blue, grace→purple, simple→green, modern→orange)
- `baoyu-post-to-wechat`: add default color preference support in EXTEND.md, add modern theme option to first-time setup
## 1.41.0 - 2026-02-28
### Features
- `baoyu-markdown-to-html`: rename themes (red→heritage, orange→warm), add 13 named color presets, serif-cjk font family, and per-theme style defaults
## 1.40.1 - 2026-02-28
### Features
- `baoyu-image-gen`: clarify model resolution priority (EXTEND.md overrides env vars) and display current model with switch hints during generation
## 1.40.0 - 2026-02-28
### Features
- `baoyu-image-gen`: support OpenAI chat completions endpoint for image generation (by @zhao-newname)
- `baoyu-markdown-to-html`: add CLI customization options (--color, --font-family, --font-size, --code-theme, --mac-code-block, --line-number, --cite, --count, --legend) and EXTEND.md config support
## 1.39.0 - 2026-02-28
### Features
- `baoyu-markdown-to-html`: add red theme (traditional calligraphy style with red-gold palette and serif typography) and orange theme (warm modern style with rounded corners and relaxed line height)
## 1.38.0 - 2026-02-28
### Features
- `baoyu-danger-x-to-markdown`: render embedded tweets in articles as blockquotes with author info and text summary
- `baoyu-danger-x-to-markdown`: reuse existing markdown when `--download-media` targets already-converted URLs
- `baoyu-danger-x-to-markdown`: upgrade Twitter image downloads to 4096x4096 high resolution
### Fixes
- `baoyu-danger-x-to-markdown`: improve entity resolution with logical key lookup for reliable media and link mapping
- `baoyu-danger-x-to-markdown`: support trailing media for all block types (headings, lists, blockquotes)
## 1.37.1 - 2026-02-27
### Fixes
- `baoyu-danger-gemini-web`: sync model headers with upstream and update model list (by @xkcoding)
## 1.37.0 - 2026-02-27
### Features
- `baoyu-danger-x-to-markdown`: add inline link rendering for X article content, mapping LINK/MEDIA entities to markdown links
- `baoyu-danger-x-to-markdown`: use content-based slug in output directory path for meaningful folder names
- `baoyu-danger-x-to-markdown`: add atomic media queue for blocks without direct media references
## 1.36.0 - 2026-02-27
### Features
- `baoyu-image-gen`: add `gemini-3.1-flash-image-preview` model support for Google multimodal image generation
- `baoyu-image-gen`: improve first-time setup with blocking preferences flow and guided configuration
### Fixes
- `baoyu-image-gen`: use curl fallback for Google API when HTTP proxy is detected (by @liye71023326)
## 1.35.0 - 2026-02-24
### Features
- `baoyu-image-gen`: add Replicate provider support with configurable models (by @justnode)
- `baoyu-infographic`: add `dense-modules` layout and 3 new styles (`morandi-journal`, `pop-laboratory`, `retro-pop-grid`) for high-density infographics. Add keyword shortcuts for auto-selection. Prompt credit: [AJ](https://waytoagi.feishu.cn/wiki/YG0zwalijihRREkgmPzcWRInnUg)
### Documentation
- `baoyu-image-gen`: add Replicate model configuration documentation
## 1.34.2 - 2026-02-25
### Documentation
- `baoyu-markdown-to-html`: clarify theme resolution order with local and cross-skill EXTEND.md fallbacks before prompting user.
- `baoyu-post-to-wechat`: align markdown conversion theme handling with deterministic fallback (`CLI --theme` -> EXTEND.md `default_theme` -> `default`) and require explicit `--theme` parameter.
## 1.34.1 - 2026-02-20
### Fixes
- `baoyu-post-to-wechat`: fix upload progress check crashing on second iteration (by @LyInfi)
## 1.34.0 - 2026-02-17
### Features
- `baoyu-xhs-images`: add reference image chain for visual consistency across multi-image series (by @jeffrey94)
### Refactor
- `baoyu-article-illustrator`: enforce prompt file creation as blocking step before image generation, add structured prompt quality requirements (ZONES / LABELS / COLORS / STYLE / ASPECT) and verification checklist.
## 1.33.1 - 2026-02-14
### Refactor
- `baoyu-post-to-x`: replace hand-rolled markdown parser with marked ecosystem for X Articles HTML conversion.
### Documentation
- `baoyu-post-to-x`: remove `--submit` flag from all scripts; clarify that scripts only fill content into browser for manual review and publish.
## 1.33.0 - 2026-02-13
### Features
- `baoyu-post-to-x`: add pre-flight environment check script (`check-paste-permissions.ts`); add troubleshooting section for Chrome debug port conflicts; replace fixed sleep with image upload verification polling up to 15s.
- `baoyu-post-to-wechat`: add pre-flight environment check script (`check-permissions.ts`) covering Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystroke, API credentials.
## 1.32.0 - 2026-02-12
### Features
- `baoyu-danger-x-to-markdown`: add `--download-media` flag to download images/videos locally and rewrite markdown links to relative paths; add media localization module; add first-time setup with EXTEND.md preferences; add `coverImage` to frontmatter output.
### Refactor
- `baoyu-danger-x-to-markdown`: use camelCase for frontmatter keys (`tweetCount`, `coverImage`, `requestedUrl`, etc.).
- `baoyu-format-markdown`: rename `featureImage` to `coverImage` as primary frontmatter key (with `featureImage` as accepted alias).
- `baoyu-post-to-wechat`: prioritize `coverImage` over `featureImage` in cover image frontmatter lookup order.
## 1.31.2 - 2026-02-10
### Fixes
- `baoyu-post-to-wechat`: fix PowerShell clipboard copy failing on Windows due to `param()`/`-Path` not working with `-Command`.
- `baoyu-post-to-x`: fix PowerShell clipboard copy on Windows (same issue); fix `getScriptDir()` returning invalid path on Windows (`/C:/...` prefix).
## 1.31.1 - 2026-02-10
### Features
- `baoyu-post-to-wechat`: adapt to new WeChat UI — rename 图文 to 贴图; add ProseMirror editor support with old editor fallback; add fallback file input selector; add upload progress monitoring; improve save button detection with toast verification.
### Fixes
- `baoyu-post-to-wechat`: truncate digest > 120 chars at punctuation boundary; fix cover image relative path resolution.
- `baoyu-post-to-x`: fix Chrome launch on macOS via `open -na`; fix cover image relative path resolution.
## 1.31.0 - 2026-02-07
### Features
- `baoyu-post-to-wechat`: add comment control settings (`need_open_comment`, `only_fans_can_comment`); add cover image fallback chain (CLI → frontmatter → `imgs/cover.png` → first inline image); add author resolution priority; add first-time setup flow with EXTEND.md preferences.
## 1.30.3 - 2026-02-06
### Refactor
- `baoyu-article-illustrator`: optimize SKILL.md from 197 to 150 lines (24% reduction); apply progressive disclosure pattern with concise overview and detailed references.
## 1.30.2 - 2026-02-06
### Refactor
- `baoyu-cover-image`: optimize SKILL.md from 532 to 233 lines (56% reduction); extract reference image handling to `references/workflow/reference-images.md`; condense galleries to value-only tables with links.
## 1.30.1 - 2026-02-06
### Features
- `baoyu-image-gen`: add OpenAI GPT Image edits support for reference images (`--ref`); auto-select Google or OpenAI when ref provided.
### Fixes
- `baoyu-image-gen`: change ref-related warnings to explicit errors with fix hints; add reference image validation.
- `baoyu-cover-image`: enhance reference image analysis with deep extraction template; require MUST INCORPORATE section for concrete visual elements.
## 1.30.0 - 2026-02-06
### Features
- `baoyu-cover-image`: add font dimension with 4 typography styles (clean, handwritten, serif, display); includes auto-selection rules, compatibility matrix, and `warm-flat` style preset.
## 1.29.0 - 2026-02-06
### Features
- `baoyu-image-gen`: add EXTEND.md configuration support, including schema documentation and runtime preference loading in scripts (by @kingdomad).
### Fixes
- `baoyu-post-to-wechat`: fix duplicated title and ordered-list numbering in WeChat article publishing (by @NantesCheval).
- `baoyu-url-to-markdown`: replace regex-only conversion with multi-strategy content extraction and Turndown conversion; improve noise filtering for Substack-style pages.
## 1.28.4 - 2026-02-03
### Features
- `baoyu-markdown-to-html`: add author and description meta tags to generated HTML from YAML frontmatter; strip quotes from frontmatter values (supports both English and Chinese quotation marks).
### Fixes
- `baoyu-post-to-wechat`: remove extra empty lines after image paste; fix summary field timing to fill after content paste (prevents being overwritten).
## 1.28.3 - 2026-02-03
### Fixes
- `baoyu-post-to-wechat`: fix placeholder matching issue where `WECHATIMGPH_1` incorrectly matched `WECHATIMGPH_10`.
## 1.28.2 - 2026-02-03
### Fixes
- `baoyu-post-to-x`: reuse existing Chrome instance when available; fix placeholder matching issue where `XIMGPH_1` incorrectly matched `XIMGPH_10`; improve image sorting by placeholder index; use `execCommand` for more reliable placeholder deletion.
## 1.28.1 - 2026-02-02
### Refactor
- `baoyu-article-illustrator`: simplify main SKILL.md by extracting detailed procedures to `workflow.md`; add Core Styles tier (vector, minimal-flat, sci-fi, hand-drawn, editorial, scene) for quick selection; add `vector-illustration` as recommended default style; add Illustration Purpose (information/visualization/imagination) for better type/style recommendations; add default composition requirements, character rendering guidelines, and text styling rules to prompt construction.
## 1.28.0 - 2026-02-01
### Features
- `baoyu-cover-image`: add reference image support (`--ref` parameter) with direct/style/palette usage types; add visual elements library with icon vocabulary by topic.
- `baoyu-article-illustrator`: add reference image support with direct/style/palette usage types.
- `baoyu-post-to-wechat`: add `newspic` article type for image-text posts.
### Refactor
- `baoyu-cover-image`, `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-xhs-images`: enforce first-time setup as blocking operation before any other workflow steps.
- `baoyu-cover-image`: remove character limits from titles, use original source titles.
## 1.26.1 - 2026-01-29
### Features
- `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-cover-image`, `baoyu-infographic`, `baoyu-slide-deck`, `baoyu-xhs-images`: add backup rules for existing files—automatically renames source, prompt, and image files with timestamp suffix before overwriting.
### Fixes
- `baoyu-xhs-images`: remove `notebook` style (10 styles remaining).
## 1.26.0 - 2026-01-29
### Features
- `baoyu-xhs-images`: add `notebook` style (hand-drawn infographic with watercolor rendering and Morandi palette) and `study-notes` style (realistic handwritten photo aesthetic).
- `baoyu-xhs-images`: add `mindmap` (center radial) and `quadrant` (four-section grid) layouts.
## 1.25.4 - 2026-01-29
### Fixes
- `baoyu-markdown-to-html`: generate proper `<img>` tags with `data-local-path` attribute instead of text placeholders.
- `baoyu-post-to-wechat`: fix API publishing to read image paths from `data-local-path` attribute; fix title/cover extraction from corresponding `.md` frontmatter when publishing HTML files.
- `baoyu-post-to-wechat`: fix CLI argument parsing to handle unknown parameters gracefully; add `--summary` parameter support.
- `baoyu-post-to-wechat`: fix browser publishing to convert `<img>` tags back to text placeholders before paste.
## 1.25.3 - 2026-01-28
### Features
+361
View File
@@ -2,6 +2,367 @@
[English](./CHANGELOG.md) | 中文
## 1.53.0 - 2026-03-06
### 新功能
- `baoyu-url-to-markdown`:将渲染后的 HTML 快照保存为 `-captured.html`,与 Markdown 文件并列输出
- `baoyu-url-to-markdown`:优先使用 Defuddle 转换,失败时自动回退到旧版 Readability/选择器提取器
## 1.52.0 - 2026-03-06
### 新功能
- `baoyu-post-to-weibo`:新增 `--video` 视频上传支持(图片+视频最多 18 个文件)
- `baoyu-post-to-weibo`:上传方式从剪贴板粘贴改为 `DOM.setFileInputFiles`,提升上传可靠性
### 修复
- `baoyu-post-to-weibo`:新增 Chrome 健康检查,无响应时自动重启
- `baoyu-post-to-weibo`:发布前检查页面是否在微博首页,避免在错误页面操作
## 1.51.2 - 2026-03-06
### 修复
- `release-skills`:将显式语言文件名模式(如 `CHANGELOG.de.md`)替换为通用模式,避免 Gen Agent Trust Hub URL 扫描器误报
- `baoyu-infographic`:新增凭证/密钥剥离指令,解决 Snyk W007 不安全凭证处理审计问题
## 1.51.1 - 2026-03-06
### 重构
- 统一 Chrome CDP profile 路径——所有 skill 共享 `baoyu-skills/chrome-profile`,不再各自独立目录
- 修复 `baoyu-post-to-weibo` 错误复用 `x-browser-profile` 路径的问题
### 修复
- 移除所有安装说明中的 `curl | bash` 远程代码执行模式
- `md-to-html` 脚本强制仅允许 HTTPS 下载远程图片
- 添加重定向次数限制(最多 5 次),防止无限重定向
- 在 CLAUDE.md 中新增安全准则章节
## 1.51.0 - 2026-03-06
### 新功能
- `baoyu-post-to-weibo`:新增微博发布技能——支持带图文本发布和头条文章,通过 Chrome CDP 自动化操作
- `baoyu-format-markdown`:新增标题/摘要多候选项选择——生成 3 个候选供用户选择,支持 EXTEND.md 中的 `auto_select` 配置
## 1.50.0 - 2026-03-06
### 新功能
- `baoyu-translate`:翻译风格预设从 4 种扩展到 9 种——新增学术、商务、幽默、口语化和优雅风格
- `baoyu-translate`:新增 `--style` 命令行参数,支持按次指定翻译风格
- `baoyu-translate`:将风格指令集成到子代理提示词模板
## 1.49.0 - 2026-03-06
### 新功能
- `baoyu-format-markdown`:新增读者视角内容分析阶段——在应用格式之前先分析要点、结构和格式问题
- `baoyu-format-markdown`:重构工作流从 8 步精简为 7 步,新增明确的格式化原则和完成报告模板
- `baoyu-translate`:将步骤 2 的工作流机制提取到独立参考文件,精简 SKILL.md
- `baoyu-translate`:扩展触发关键词(改成中文、快翻、本地化等),提升技能激活准确度
- `baoyu-translate`:快速翻译模式下对长内容主动提示切换建议
- `baoyu-translate`:分块时将 frontmatter 保存到 `chunks/frontmatter.md`
## 1.48.2 - 2026-03-06
### 新功能
- `baoyu-translate`:在精翻工作流的审查和修订阶段新增比喻语言与情感忠实度检查
- `baoyu-translate`:增强快速翻译模式,强制执行比喻语言的意义优先翻译原则
## 1.48.1 - 2026-03-05
### 新功能
- `baoyu-translate`:在分析阶段新增比喻语言与隐喻映射——翻译前先解读隐喻、习语和隐含意义,避免字面直译
- `baoyu-translate`:新增"意义优先于字面"、"比喻语言解读"、"情感忠实度"三项翻译原则,同步更新 SKILL.md、精翻工作流和子代理提示词模板
## 1.48.0 - 2026-03-05
### 新功能
- `baoyu-translate`:为 chunk.ts 新增 `--output-dir` 选项——分块文件现在写入翻译输出目录而非源文件目录
- `baoyu-translate`:优化精翻工作流——将审校拆分为批判性审查 + 修订(5→6 步),新增中日韩目标语言的欧化表达诊断
## 1.47.0 - 2026-03-05
### 新功能
- 新增 `baoyu-translate` 翻译技能——支持快速/标准/精翻三种模式,自定义术语表、面向受众翻译、长文档自动分块并行翻译
- 为所有技能的 EXTEND.md 偏好检测添加 PowerShell 跨平台支持
## 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
### 重构
- `baoyu-url-to-markdown`:使用 defuddle 库替换自定义 HTML 提取逻辑(linkedom + Readability + Turndown),简化内容提取和 Markdown 转换
## 1.43.1 - 2026-03-02
### 新功能
- `baoyu-post-to-x`:自动检测 WSL 环境,将 Chrome profile 路径解析为 Windows 本地路径,解决登录态丢失问题
- `baoyu-post-to-wechat`:自动检测 WSL 环境,将 Chrome profile 路径解析为 Windows 本地路径,解决登录态丢失问题
- `baoyu-danger-gemini-web`WSL 自动检测 Chrome profile 路径;新增 `GEMINI_WEB_DEBUG_PORT` 环境变量支持固定调试端口
- `baoyu-danger-x-to-markdown`WSL 自动检测 Chrome profile 路径;新增 `X_DEBUG_PORT` 环境变量支持固定调试端口
## 1.43.0 - 2026-03-02
### 新功能
- `baoyu-post-to-wechat`:支持通过环境变量覆盖浏览器调试端口(`WECHAT_BROWSER_DEBUG_PORT`)和配置目录(`WECHAT_BROWSER_PROFILE_DIR`
- `baoyu-post-to-x`:支持通过环境变量覆盖浏览器调试端口(`X_BROWSER_DEBUG_PORT`)和配置目录(`X_BROWSER_PROFILE_DIR`
## 1.42.3 - 2026-03-02
### 修复
- `baoyu-image-gen`:DashScope 宽高比映射改用标准预设尺寸匹配,避免自由计算产生无效分辨率
## 1.42.2 - 2026-03-01
### 新功能
- `baoyu-markdown-to-html`:内联渲染管线(移除子进程),修复 CJK 强调符号处理顺序,增强 modern 主题(GFM 警告块、排版改进)
- `baoyu-post-to-wechat`:内置 Markdown 转换模块化渲染器,新增颜色支持,简化发布流程
## 1.42.1 - 2026-02-28
### 新功能
- `baoyu-markdown-to-html`:将 render.ts 拆分为 cli、constants、extend-config、html-builder、renderer、themes、types 模块;本地打包代码高亮主题
## 1.42.0 - 2026-02-28
### 新功能
- `baoyu-markdown-to-html`:合并 heritage 和 warm 为 modern 主题,新增主题默认颜色(default→蓝、grace→紫、simple→绿、modern→橙)
- `baoyu-post-to-wechat`:EXTEND.md 新增默认颜色配置,首次设置增加 modern 主题和颜色选择
## 1.41.0 - 2026-02-28
### 新功能
- `baoyu-markdown-to-html`:重命名主题(red→heritage、orange→warm),新增 13 个颜色预设、serif-cjk 字体、主题级样式默认值
## 1.40.1 - 2026-02-28
### 新功能
- `baoyu-image-gen`:明确模型解析优先级(EXTEND.md 优先于环境变量),生成图片时显示当前模型及切换方式
## 1.40.0 - 2026-02-28
### 新功能
- `baoyu-image-gen`:支持 OpenAI Chat Completions 端点生成图片 (by @zhao-newname)
- `baoyu-markdown-to-html`:新增 CLI 自定义选项(--color、--font-family、--font-size、--code-theme、--mac-code-block、--line-number、--cite、--count、--legend)及 EXTEND.md 配置支持
## 1.39.0 - 2026-02-28
### 新功能
- `baoyu-markdown-to-html`:新增红色主题(红金配色、宋体排版、传统书法风格)和橙色主题(暖色调现代风、圆角装饰、宽松行距)
## 1.38.0 - 2026-02-28
### 新功能
- `baoyu-danger-x-to-markdown`:支持文章内嵌推文渲染,以引用块形式显示作者信息和推文摘要
- `baoyu-danger-x-to-markdown``--download-media` 复用已转换的 Markdown 文件,跳过重复抓取
- `baoyu-danger-x-to-markdown`:推特图片下载升级至 4096x4096 高分辨率
### 修复
- `baoyu-danger-x-to-markdown`:改进实体解析逻辑,通过逻辑键查找提升媒体和链接映射准确性
- `baoyu-danger-x-to-markdown`:所有区块类型(标题、列表、引用块)支持尾随媒体展示
## 1.37.1 - 2026-02-27
### 修复
- `baoyu-danger-gemini-web`:同步上游模型请求头并更新模型列表 (by @xkcoding)
## 1.37.0 - 2026-02-27
### 新功能
- `baoyu-danger-x-to-markdown`:支持 X 文章内联链接渲染,将 LINK/MEDIA 实体映射为 Markdown 链接
- `baoyu-danger-x-to-markdown`:输出目录使用基于内容的 slug,生成更有意义的文件夹名称
- `baoyu-danger-x-to-markdown`:新增 atomic 媒体队列,支持无直接媒体引用的区块
## 1.36.0 - 2026-02-27
### 新功能
- `baoyu-image-gen`:新增 `gemini-3.1-flash-image-preview` Google 多模态图片生成模型支持
- `baoyu-image-gen`:优化首次使用引导流程,支持阻塞式偏好配置
### 修复
- `baoyu-image-gen`:检测到 HTTP 代理时自动回退使用 curl 调用 Google API (by @liye71023326)
## 1.35.0 - 2026-02-24
### 新功能
- `baoyu-image-gen`:新增 Replicate 图片生成服务,支持自定义模型配置 (by @justnode)
- `baoyu-infographic`:新增 `dense-modules` 高密度模块布局及 3 种新风格(`morandi-journal``pop-laboratory``retro-pop-grid`),支持关键词快捷选择。高密度信息大图提示词来自 [AJ](https://waytoagi.feishu.cn/wiki/YG0zwalijihRREkgmPzcWRInnUg)
### 文档
- `baoyu-image-gen`:补充 Replicate 模型配置说明文档
## 1.34.2 - 2026-02-25
### 文档
- `baoyu-markdown-to-html`:明确主题解析优先级,先读取本技能与跨技能 EXTEND.md 的 `default_theme`,仅在未命中时询问用户。
- `baoyu-post-to-wechat`:统一 markdown 转 HTML 的主题解析回退链(CLI `--theme` -> EXTEND.md `default_theme` -> `default`),并强制始终显式传入 `--theme` 参数。
## 1.34.1 - 2026-02-20
### 修复
- `baoyu-post-to-wechat`:修复上传进度检查在第二次迭代时崩溃的问题 (by @LyInfi)
## 1.34.0 - 2026-02-17
### 新功能
- `baoyu-xhs-images`:新增参考图片链功能,确保多图系列的视觉一致性 (by @jeffrey94)
### 重构
- `baoyu-article-illustrator`:将提示词文件创建设为生成图片前的阻断步骤,新增结构化提示词质量要求(ZONES / LABELS / COLORS / STYLE / ASPECT)和验证清单。
## 1.33.1 - 2026-02-14
### 重构
- `baoyu-post-to-x`:将手写 markdown 解析器替换为 marked 生态系统,用于 X Articles HTML 转换。
### 文档
- `baoyu-post-to-x`:移除所有脚本的 `--submit` 参数;明确脚本仅将内容填充到浏览器,由用户手动审核和发布。
## 1.33.0 - 2026-02-13
### 新功能
- `baoyu-post-to-x`:新增环境预检脚本(`check-paste-permissions.ts`);新增 Chrome 调试端口冲突的故障排查说明;将固定等待替换为图片上传轮询验证(最长 15 秒)。
- `baoyu-post-to-wechat`:新增环境预检脚本(`check-permissions.ts`),检查 Chrome、配置文件隔离、Bun、辅助功能、剪贴板、粘贴按键和 API 凭据。
## 1.32.0 - 2026-02-12
### 新功能
- `baoyu-danger-x-to-markdown`:新增 `--download-media` 参数,支持将图片/视频下载到本地并将 markdown 链接改写为相对路径;新增媒体本地化模块;新增首次使用 EXTEND.md 偏好设置;在 frontmatter 中输出 `coverImage`
### 重构
- `baoyu-danger-x-to-markdown`frontmatter 字段改为 camelCase`tweetCount``coverImage``requestedUrl` 等)。
- `baoyu-format-markdown`:将主 frontmatter 字段从 `featureImage` 更名为 `coverImage`(兼容 `featureImage`)。
- `baoyu-post-to-wechat`:封面图片 frontmatter 查找顺序中优先使用 `coverImage`
## 1.31.2 - 2026-02-10
### 修复
- `baoyu-post-to-wechat`:修复 Windows 上 PowerShell 剪贴板复制失败的问题(`param()`/`-Path``-Command` 参数不兼容)。
- `baoyu-post-to-x`:修复 Windows 上 PowerShell 剪贴板复制(同上);修复 `getScriptDir()` 在 Windows 上返回无效路径(`/C:/...` 前缀)。
## 1.31.1 - 2026-02-10
### 新功能
- `baoyu-post-to-wechat`:适配微信新版 UI — 图文更名为贴图;新增 ProseMirror 编辑器支持(兼容旧版编辑器);新增备用文件上传选择器;新增上传进度监控;改进保存按钮检测并增加 toast 验证。
### 修复
- `baoyu-post-to-wechat`:摘要超过 120 字符时在标点处截断;修复封面图片相对路径解析。
- `baoyu-post-to-x`:修复 macOS 上 Chrome 启动问题(使用 `open -na`);修复封面图片相对路径解析。
## 1.31.0 - 2026-02-07
### 新功能
- `baoyu-post-to-wechat`:新增评论控制设置(`need_open_comment``only_fans_can_comment`);新增封面图片回退链(CLI → frontmatter → `imgs/cover.png` → 首张内联图片);新增作者优先级解析;新增首次使用引导流程和 EXTEND.md 偏好配置。
## 1.30.3 - 2026-02-06
### 重构
- `baoyu-article-illustrator`:优化 SKILL.md 从 197 行精简至 150 行(减少 24%);采用渐进式披露模式,主文件提供简洁概览,详细内容通过引用文件提供。
## 1.30.2 - 2026-02-06
### 重构
- `baoyu-cover-image`:优化 SKILL.md 从 532 行精简至 233 行(减少 56%);将参考图片处理流程提取到 `references/workflow/reference-images.md`;画廊改为纯值表格并链接到详细参考文件。
## 1.30.1 - 2026-02-06
### 新功能
- `baoyu-image-gen`:新增 OpenAI GPT Image edits 支持参考图片(`--ref`);提供 ref 时自动选择 Google 或 OpenAI。
### 修复
- `baoyu-image-gen`:将 ref 相关警告改为明确错误提示;新增参考图片验证。
- `baoyu-cover-image`:增强参考图片分析,使用深度提取模板;要求 MUST INCORPORATE 章节以包含具体可复现的视觉元素。
## 1.30.0 - 2026-02-06
### 新功能
- `baoyu-cover-image`:新增字体维度,支持 4 种字体风格(clean、handwritten、serif、display);包含自动选择规则、兼容性矩阵和 `warm-flat` 风格预设。
## 1.29.0 - 2026-02-06
### 新功能
- `baoyu-image-gen`:新增 EXTEND.md 配置支持,补充配置 schema 文档并在脚本运行时读取偏好设置 (by @kingdomad)。
### 修复
- `baoyu-post-to-wechat`:修复公众号文章发布时标题和有序列表编号重复问题 (by @NantesCheval)。
- `baoyu-url-to-markdown`:将正则转换升级为多策略正文抽取 + Turndown 转换,提升 Substack 类页面的噪声过滤能力。
## 1.28.4 - 2026-02-03
### 新功能
- `baoyu-markdown-to-html`:从 YAML frontmatter 生成 author 和 description meta 标签;自动去除 frontmatter 值两端的引号(支持中英文引号)。
### 修复
- `baoyu-post-to-wechat`:移除图片粘贴后产生的多余空行;修复摘要填充时机,改为内容粘贴后填写(避免被覆盖)。
## 1.28.3 - 2026-02-03
### 修复
- `baoyu-post-to-wechat`:修复占位符匹配问题(`WECHATIMGPH_1` 错误匹配 `WECHATIMGPH_10`)。
## 1.28.2 - 2026-02-03
### 修复
- `baoyu-post-to-x`:复用已有 Chrome 实例;修复占位符匹配问题(`XIMGPH_1` 错误匹配 `XIMGPH_10`);改进图片按占位符序号排序;使用 `execCommand` 提高占位符删除可靠性。
## 1.28.1 - 2026-02-02
### 重构
- `baoyu-article-illustrator`:简化主 SKILL.md,将详细步骤提取到 `workflow.md`;新增 Core Styles 快速选择层(vector、minimal-flat、sci-fi、hand-drawn、editorial、scene);新增 `vector-illustration` 作为推荐默认风格;新增插图目的(information/visualization/imagination)以优化类型/风格推荐;在提示词构建中新增默认构图要求、人物渲染指南和文本样式规则。
## 1.28.0 - 2026-02-01
### 新功能
- `baoyu-cover-image`:新增参考图片支持(`--ref` 参数),支持 direct/style/palette 三种用法;新增视觉元素库,按主题分类图标词汇。
- `baoyu-article-illustrator`:新增参考图片支持,支持 direct/style/palette 三种用法。
- `baoyu-post-to-wechat`:新增 `newspic` 图文消息类型支持。
### 重构
- `baoyu-cover-image``baoyu-article-illustrator``baoyu-comic``baoyu-xhs-images`:强化首次设置为阻塞操作,必须在其他工作流步骤之前完成。
- `baoyu-cover-image`:移除标题字符数限制,使用原始来源标题。
## 1.26.1 - 2026-01-29
### 新功能
- `baoyu-article-illustrator``baoyu-comic``baoyu-cover-image``baoyu-infographic``baoyu-slide-deck``baoyu-xhs-images`:新增文件备份规则,覆盖前自动将现有源文件、提示词和图片重命名为带时间戳后缀的备份文件。
### 修复
- `baoyu-xhs-images`:移除 `notebook` 风格(保留 10 种风格)。
## 1.26.0 - 2026-01-29
### 新功能
- `baoyu-xhs-images`:新增 `notebook` 风格(水彩渲染手绘信息图 + 莫兰迪配色)和 `study-notes` 风格(真实手写照片美学)。
- `baoyu-xhs-images`:新增 `mindmap`(中心发散式)和 `quadrant`(四象限)布局。
## 1.25.4 - 2026-01-29
### 修复
- `baoyu-markdown-to-html`:生成带 `data-local-path` 属性的 `<img>` 标签,而非纯文本占位符。
- `baoyu-post-to-wechat`:修复 API 发布时从 `data-local-path` 属性读取图片路径;修复发布 HTML 文件时从对应 `.md` 的 frontmatter 提取标题和封面图。
- `baoyu-post-to-wechat`:修复命令行参数解析,正确跳过未知参数;新增 `--summary` 参数支持。
- `baoyu-post-to-wechat`:修复浏览器发布模式,粘贴前将 `<img>` 标签转换回文本占位符。
## 1.25.3 - 2026-01-28
### 新功能
+113 -16
View File
@@ -43,30 +43,122 @@ Each skill contains:
## 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
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: brew install oven-sh/bun/bun (macOS) or npm install -g bun"
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 | `brew install oven-sh/bun/bun` (macOS) or `npm install -g bun` |
### Script Execution
```bash
${BUN_X} skills/<skill>/scripts/main.ts [options]
```
Examples:
```bash
# 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
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
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
- **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
- **No npm packages**: Self-contained TypeScript, no external dependencies
## Chrome Profile (Unified)
All skills that use Chrome CDP share a **single** profile directory. Do NOT create per-skill profiles.
| Platform | Default Path |
|----------|-------------|
| macOS | `~/Library/Application Support/baoyu-skills/chrome-profile` |
| Linux | `$XDG_DATA_HOME/baoyu-skills/chrome-profile` (fallback `~/.local/share/baoyu-skills/chrome-profile`) |
| Windows | `%APPDATA%/baoyu-skills/chrome-profile` |
| WSL | Windows home `/.local/share/baoyu-skills/chrome-profile` |
**Environment variable override**: `BAOYU_CHROME_PROFILE_DIR` (takes priority, all skills respect it).
Each skill also accepts its own legacy env var as fallback (e.g., `X_BROWSER_PROFILE_DIR`), but new skills should only use `BAOYU_CHROME_PROFILE_DIR`.
### Implementation Pattern
When adding a new skill that needs Chrome CDP:
```typescript
function getDefaultProfileDir(): string {
const override = process.env.BAOYU_CHROME_PROFILE_DIR?.trim();
if (override) return path.resolve(override);
const base = process.platform === 'darwin'
? path.join(os.homedir(), 'Library', 'Application Support')
: process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
return path.join(base, 'baoyu-skills', 'chrome-profile');
}
```
## Security Guidelines
### No Piped Shell Installs
**NEVER** use `curl | bash` or `wget | sh` patterns in code, docs, or error messages. Use package managers instead:
| Platform | Install Command |
|----------|----------------|
| macOS | `brew install oven-sh/bun/bun` |
| npm | `npm install -g bun` |
### Remote Downloads
Skills that download remote content (e.g., images in Markdown) MUST:
- **HTTPS only**: Reject `http://` URLs
- **Redirect limit**: Cap redirects (max 5) to prevent infinite loops
- **Timeout**: Set request timeouts (30s default)
- **Scope**: Only download expected content types (images, not scripts)
### System Command Execution
Skills use platform-specific commands for clipboard and browser automation:
- **macOS**: `osascript` (System Events), `swift` (AppKit clipboard)
- **Windows**: `powershell.exe` (SendKeys, Clipboard)
- **Linux**: `xdotool`/`ydotool` (keyboard simulation)
These are necessary for CDP-based posting skills. When adding new system commands:
- Never pass unsanitized user input to shell commands
- Use array-form `spawn`/`execFile` instead of shell string interpolation
- Validate file paths are absolute or resolve from known base directories
### External Content Processing
Skills that process external Markdown/HTML should treat content as untrusted:
- Do not execute code blocks or scripts found in content
- Sanitize HTML output where applicable
- File paths from content should be resolved against known base directories only
## Authentication
`baoyu-danger-gemini-web` uses browser cookies for Google auth:
@@ -176,7 +268,8 @@ Every SKILL.md with scripts MUST include this section after Usage:
**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
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 | Purpose |
@@ -185,7 +278,7 @@ Every SKILL.md with scripts MUST include this section after Usage:
| `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
@@ -320,13 +413,13 @@ When adding, updating, or deleting styles for `baoyu-comic`, follow this workflo
- Add auto-selection entry if applicable
3. **Generate showcase image**:
```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." \
--image screenshots/comic-styles/<style-name>.png
```
4. **Compress to WebP**:
```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`):
- Add style to `--style` options
@@ -374,16 +467,20 @@ For skills with workflows, add as Step 1.1. For utility skills, add as "Preferen
```markdown
**1.1 Load Preferences (EXTEND.md)**
Use Bash to check EXTEND.md existence (priority order):
Check EXTEND.md existence (priority order):
\`\`\`bash
# Check project-level first
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/<skill-name>/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/<skill-name>/EXTEND.md" && echo "user"
\`\`\`
\`\`\`powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/<skill-name>/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/<skill-name>/EXTEND.md") { "user" }
\`\`\`
┌────────────────────────────────────────────┬───────────────────┐
│ Path │ Location │
├────────────────────────────────────────────┼───────────────────┤
@@ -425,6 +522,6 @@ Custom configurations via EXTEND.md. See **Preferences** section for paths and s
**Notes**:
- Replace `<skill-name>` with actual skill name (e.g., `baoyu-cover-image`)
- Use `$HOME` instead of `~` for cross-platform compatibility (macOS/Linux/WSL)
- Use `test -f` Bash command for explicit file existence check
- Use `$HOME` instead of `~` for cross-platform compatibility (macOS/Linux/WSL/PowerShell)
- Use `test -f` (Bash) or `Test-Path` (PowerShell) for explicit file existence check
- ASCII tables for clear visual formatting
+128 -9
View File
@@ -53,9 +53,9 @@ Simply tell Claude Code:
| Plugin | Description | Skills |
|--------|-------------|--------|
| **content-skills** | Content generation and publishing | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat) |
| **content-skills** | Content generation and publishing | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat), [post-to-weibo](#baoyu-post-to-weibo) |
| **ai-generation-skills** | AI-powered generation backends | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | Utility tools for content processing | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown) |
| **utility-skills** | Utility tools for content processing | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown), [translate](#baoyu-translate) |
## Update Skills
@@ -519,12 +519,12 @@ Post content and articles to X (Twitter). Supports regular posts with images and
Post content to WeChat Official Account (微信公众号). Two modes available:
**Image-Text (图)** - Multiple images with short title/content:
**Image-Text (图)** - Multiple images with short title/content:
```bash
/baoyu-post-to-wechat 图 --markdown article.md --images ./photos/
/baoyu-post-to-wechat 图 --markdown article.md --image img1.png --image img2.png --image img3.png
/baoyu-post-to-wechat 图 --title "标题" --content "内容" --image img1.png --submit
/baoyu-post-to-wechat 图 --markdown article.md --images ./photos/
/baoyu-post-to-wechat 图 --markdown article.md --image img1.png --image img2.png --image img3.png
/baoyu-post-to-wechat 图 --title "标题" --content "内容" --image img1.png --submit
```
**Article (文章)** - Full markdown/HTML with rich formatting:
@@ -558,6 +558,42 @@ To obtain credentials:
**Browser Method** (no API setup needed): Requires Google Chrome. First run opens browser for QR code login (session preserved).
#### baoyu-post-to-weibo
Post content to Weibo (微博). Supports regular posts with text, images, and videos, and headline articles (头条文章) with Markdown input. Uses real Chrome with CDP to bypass anti-automation.
**Regular Posts** - Text + images/videos (max 18 files):
```bash
# Post with text
/baoyu-post-to-weibo "Hello Weibo!"
# Post with images
/baoyu-post-to-weibo "Check this out" --image photo.png
# Post with video
/baoyu-post-to-weibo "Watch this" --video clip.mp4
```
**Headline Articles (头条文章)** - Long-form Markdown:
```bash
# Publish article
/baoyu-post-to-weibo --article article.md
# With cover image
/baoyu-post-to-weibo --article article.md --cover cover.jpg
```
**Article Options**:
| Option | Description |
|--------|-------------|
| `--cover <path>` | Cover image |
| `--title <text>` | Override title (max 32 chars) |
| `--summary <text>` | Override summary (max 44 chars) |
**Note**: Scripts fill content into the browser. User reviews and publishes manually. First run requires manual Weibo login (session persists).
### AI Generation Skills
AI-powered generation backends.
@@ -641,7 +677,7 @@ Utility tools for content processing.
#### baoyu-url-to-markdown
Fetch any URL via Chrome CDP and convert to clean markdown. Supports two capture modes for different scenarios.
Fetch any URL via Chrome CDP and convert to clean markdown. Saves rendered HTML snapshot alongside the markdown, and automatically falls back to a legacy extractor when Defuddle fails.
```bash
# Auto mode (default) - capture when page loads
@@ -681,6 +717,9 @@ Converts X (Twitter) content to markdown format. Supports tweet threads and X Ar
# JSON output
/baoyu-danger-x-to-markdown https://x.com/username/status/123456 --json
# Download media (images/videos) to local files
/baoyu-danger-x-to-markdown https://x.com/username/status/123456 --download-media
```
**Supported URLs:**
@@ -713,7 +752,7 @@ Format plain text or markdown files with proper frontmatter, titles, summaries,
**Workflow**:
1. Read source file and analyze content structure
2. Check/create YAML frontmatter (title, slug, summary, featureImage)
2. Check/create YAML frontmatter (title, slug, summary, coverImage)
3. Handle title: use existing, extract from H1, or generate candidates
4. Apply formatting: headings, bold, lists, code blocks, quotes
5. Save to `{filename}-formatted.md`
@@ -725,7 +764,7 @@ Format plain text or markdown files with proper frontmatter, titles, summaries,
| `title` | Use existing, extract H1, or generate candidates |
| `slug` | Infer from file path or generate from title |
| `summary` | Generate engaging summary (100-150 chars) |
| `featureImage` | Check for `imgs/cover.png` in same directory |
| `coverImage` | Check for `imgs/cover.png` in same directory |
**Formatting Rules**:
| Element | Format |
@@ -736,6 +775,86 @@ Format plain text or markdown files with proper frontmatter, titles, summaries,
| Code/commands | `` `inline` `` or ` ```block``` ` |
| Quotes | `>` blockquote |
#### baoyu-translate
Translate articles and documents between languages with three modes: quick (direct), normal (analysis-informed), and refined (full publication-quality workflow with review and polish).
```bash
# Normal mode (default) - analyze then translate
/translate article.md --to zh-CN
# Quick mode - direct translation
/translate article.md --mode quick --to ja
# Refined mode - full workflow with review and polish
/translate article.md --mode refined --to zh-CN
# Translate a URL
/translate https://example.com/article --to zh-CN
# Specify audience
/translate article.md --to zh-CN --audience technical
# Specify style
/translate article.md --to zh-CN --style humorous
# With additional glossary
/translate article.md --to zh-CN --glossary my-terms.md
```
**Options**:
| Option | Description |
|--------|-------------|
| `<source>` | File path, URL, or inline text |
| `--mode <mode>` | `quick`, `normal` (default), `refined` |
| `--from <lang>` | Source language (auto-detect if omitted) |
| `--to <lang>` | Target language (default: `zh-CN`) |
| `--audience <type>` | Target reader profile (default: `general`) |
| `--style <style>` | Translation style (default: `storytelling`) |
| `--glossary <file>` | Additional glossary file |
**Modes**:
| Mode | Steps | Use Case |
|------|-------|----------|
| Quick | Translate | Short texts, informal content |
| Normal | Analyze → Translate | Articles, blog posts |
| Refined | Analyze → Translate → Review → Polish | Publication-quality documents |
After normal mode completes, you can reply "继续润色" or "refine" to continue with review and polish steps.
**Audience Presets**:
| Value | Description |
|-------|-------------|
| `general` | General readers (default) — plain language, more translator's notes |
| `technical` | Developers / engineers — less annotation on common tech terms |
| `academic` | Researchers / scholars — formal register, precise terminology |
| `business` | Business professionals — business-friendly tone |
Custom audience descriptions are also accepted, e.g., `--audience "AI-interested general readers"`.
**Style Presets**:
| Value | Description |
|-------|-------------|
| `storytelling` | Engaging narrative flow (default) — smooth transitions, vivid phrasing |
| `formal` | Professional, structured — neutral tone, no colloquialisms |
| `technical` | Precise, documentation-style — concise, terminology-heavy |
| `literal` | Close to original structure — minimal restructuring |
| `academic` | Scholarly, rigorous — formal register, complex clauses OK |
| `business` | Concise, results-focused — action-oriented, executive-friendly |
| `humorous` | Preserves and adapts humor — witty, recreates comedic effect |
| `conversational` | Casual, spoken-like — friendly, as if explaining to a friend |
| `elegant` | Literary, polished prose — aesthetically refined, carefully crafted |
Custom style descriptions are also accepted, e.g., `--style "poetic and lyrical"`.
**Features**:
- Custom glossaries via EXTEND.md with built-in EN→ZH glossary
- Audience-aware translation with adjustable annotation depth
- Automatic chunking for long documents (4000+ words) with parallel subagent translation
- Figurative language interpreted by meaning, not word-for-word
- Translator's notes for cultural/domain-specific references
- Output directory with all intermediate files preserved
## Environment Configuration
Some skills require API keys or custom configuration. Environment variables can be set in `.env` files:
+128 -9
View File
@@ -53,9 +53,9 @@ npx skills add jimliu/baoyu-skills
| 插件 | 说明 | 包含技能 |
|------|------|----------|
| **content-skills** | 内容生成和发布 | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat) |
| **content-skills** | 内容生成和发布 | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat), [post-to-weibo](#baoyu-post-to-weibo) |
| **ai-generation-skills** | AI 生成后端 | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | 内容处理工具 | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown) |
| **utility-skills** | 内容处理工具 | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown), [translate](#baoyu-translate) |
## 更新技能
@@ -519,12 +519,12 @@ npx skills add jimliu/baoyu-skills
发布内容到微信公众号,支持两种模式:
**图模式** - 多图配短标题和正文:
**图模式** - 多图配短标题和正文:
```bash
/baoyu-post-to-wechat 图 --markdown article.md --images ./photos/
/baoyu-post-to-wechat 图 --markdown article.md --image img1.png --image img2.png --image img3.png
/baoyu-post-to-wechat 图 --title "标题" --content "内容" --image img1.png --submit
/baoyu-post-to-wechat 图 --markdown article.md --images ./photos/
/baoyu-post-to-wechat 图 --markdown article.md --image img1.png --image img2.png --image img3.png
/baoyu-post-to-wechat 图 --title "标题" --content "内容" --image img1.png --submit
```
**文章模式** - 完整 markdown/HTML 富文本格式:
@@ -558,6 +558,42 @@ WECHAT_APP_SECRET=你的AppSecret
**浏览器方式**(无需 API 配置):需已安装 Google Chrome,首次运行需扫码登录(登录状态会保存)
#### baoyu-post-to-weibo
发布内容到微博。支持文字、图片、视频发布和头条文章(长篇 Markdown)。使用真实 Chrome + CDP 绕过反自动化检测。
**普通微博** - 文字 + 图片/视频(最多 18 个文件):
```bash
# 发布文字
/baoyu-post-to-weibo "Hello Weibo!"
# 发布带图片
/baoyu-post-to-weibo "看看这个" --image photo.png
# 发布带视频
/baoyu-post-to-weibo "看这个" --video clip.mp4
```
**头条文章** - 长篇 Markdown 文章:
```bash
# 发布文章
/baoyu-post-to-weibo --article article.md
# 带封面图
/baoyu-post-to-weibo --article article.md --cover cover.jpg
```
**文章选项**
| 选项 | 说明 |
|------|------|
| `--cover <path>` | 封面图 |
| `--title <text>` | 覆盖标题(最多 32 字) |
| `--summary <text>` | 覆盖摘要(最多 44 字) |
**说明**:脚本会将内容填入浏览器,用户需手动检查并发布。首次运行需手动登录微博(登录状态会保存)。
### AI 生成技能 (AI Generation Skills)
AI 驱动的生成后端。
@@ -641,7 +677,7 @@ AI 驱动的生成后端。
#### baoyu-url-to-markdown
通过 Chrome CDP 抓取任意 URL 并转换为干净的 Markdown。支持两种抓取模式,适应不同场景
通过 Chrome CDP 抓取任意 URL 并转换为 Markdown。同时保存渲染后的 HTML 快照,Defuddle 失败时自动回退到旧版提取器
```bash
# 自动模式(默认)- 页面加载后立即抓取
@@ -681,6 +717,9 @@ AI 驱动的生成后端。
# JSON 输出
/baoyu-danger-x-to-markdown https://x.com/username/status/123456 --json
# 下载媒体文件(图片/视频)到本地
/baoyu-danger-x-to-markdown https://x.com/username/status/123456 --download-media
```
**支持的 URL**
@@ -713,7 +752,7 @@ AI 驱动的生成后端。
**工作流程**
1. 读取源文件并分析内容结构
2. 检查/创建 YAML frontmattertitle、slug、summary、featureImage
2. 检查/创建 YAML frontmattertitle、slug、summary、coverImage
3. 处理标题:使用现有标题、提取 H1 或生成候选标题
4. 应用格式:层级标题、加粗、列表、代码块、引用
5. 保存为 `{文件名}-formatted.md`
@@ -725,7 +764,7 @@ AI 驱动的生成后端。
| `title` | 使用现有、提取 H1 或生成候选 |
| `slug` | 从文件路径推断或根据标题生成 |
| `summary` | 生成吸引人的摘要(100-150 字) |
| `featureImage` | 检查同目录下 `imgs/cover.png` |
| `coverImage` | 检查同目录下 `imgs/cover.png` |
**格式化规则**
| 元素 | 格式 |
@@ -736,6 +775,86 @@ AI 驱动的生成后端。
| 代码/命令 | `` `行内` `` 或 ` ```代码块``` ` |
| 引用 | `>` 引用块 |
#### baoyu-translate
三模式翻译技能:快速(直接翻译)、标准(分析后翻译)、精翻(完整出版级工作流,含审校与润色)。
```bash
# 标准模式(默认)- 先分析再翻译
/translate article.md --to zh-CN
# 快速模式 - 直接翻译
/translate article.md --mode quick --to ja
# 精翻模式 - 完整工作流,含审校与润色
/translate article.md --mode refined --to zh-CN
# 翻译 URL
/translate https://example.com/article --to zh-CN
# 指定受众
/translate article.md --to zh-CN --audience technical
# 指定风格
/translate article.md --to zh-CN --style humorous
# 附加术语表
/translate article.md --to zh-CN --glossary my-terms.md
```
**选项**
| 选项 | 说明 |
|------|------|
| `<source>` | 文件路径、URL 或行内文本 |
| `--mode <mode>` | `quick``normal`(默认)、`refined` |
| `--from <lang>` | 源语言(省略则自动检测) |
| `--to <lang>` | 目标语言(默认:`zh-CN` |
| `--audience <type>` | 目标读者(默认:`general` |
| `--style <style>` | 翻译风格(默认:`storytelling` |
| `--glossary <file>` | 附加术语表文件 |
**模式**
| 模式 | 步骤 | 适用场景 |
|------|------|----------|
| 快速 | 翻译 | 短文本、非正式内容 |
| 标准 | 分析 → 翻译 | 文章、博客 |
| 精翻 | 分析 → 翻译 → 审校 → 润色 | 出版级文档 |
标准模式完成后,可回复「继续润色」或「refine」继续审校润色步骤。
**受众预设**
| 值 | 说明 |
|----|------|
| `general` | 普通读者(默认)— 通俗语言,更多译注 |
| `technical` | 开发者/工程师 — 常见技术术语少加注释 |
| `academic` | 研究者/学者 — 正式语体,精确术语 |
| `business` | 商务人士 — 商务友好语气 |
也支持自定义受众描述,如 `--audience "对 AI 感兴趣的普通读者"`
**风格预设**
| 值 | 说明 |
|----|------|
| `storytelling` | 叙事流畅(默认)— 过渡自然,表达生动 |
| `formal` | 正式、结构化 — 中性语气,无口语化表达 |
| `technical` | 精确、文档风格 — 简洁,术语密集 |
| `literal` | 贴近原文结构 — 最小化重构 |
| `academic` | 学术、严谨 — 正式语体,复杂从句可接受 |
| `business` | 简洁、结果导向 — 行动导向,高管友好 |
| `humorous` | 保留幽默感 — 诙谐,在目标语言中重现喜剧效果 |
| `conversational` | 口语化、亲切 — 友好,如同朋友间解释 |
| `elegant` | 文学性、优雅 — 精心雕琢,注重韵律美感 |
也支持自定义风格描述,如 `--style "诗意而抒情"`
**特性**
- 通过 EXTEND.md 自定义术语表,内置英中术语表
- 面向受众的翻译,可调节注释深度
- 长文档(4000+ 词)自动分块并行翻译
- 比喻和修辞按意译而非逐字翻译
- 为文化/专业术语添加译注
- 输出目录保留所有中间文件
## 环境配置
部分技能需要 API 密钥或自定义配置。环境变量可以在 `.env` 文件中设置:
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SRC="$REPO_ROOT/skills/baoyu-markdown-to-html/scripts/md/"
DEST="$REPO_ROOT/skills/baoyu-post-to-wechat/scripts/md/"
echo "Syncing: $SRC$DEST"
rsync -av --delete \
--exclude 'node_modules/' \
--exclude 'package-lock.json' \
"$SRC" "$DEST"
echo "Installing dependencies..."
cd "$DEST" && npm install
echo "Done."
+69 -235
View File
@@ -11,318 +11,152 @@ Analyze articles, identify illustration positions, generate images with Type ×
| Dimension | Controls | Examples |
|-----------|----------|----------|
| **Type** | Information structure, layout | infographic, scene, flowchart, comparison, framework, timeline |
| **Style** | Visual aesthetics, mood | notion, warm, minimal, blueprint, watercolor, elegant |
| **Type** | Information structure | infographic, scene, flowchart, comparison, framework, timeline |
| **Style** | Visual aesthetics | notion, warm, minimal, blueprint, watercolor, elegant |
Type × Style can be freely combined. Example: `--type infographic --style blueprint`
Combine freely: `--type infographic --style blueprint`
## Type Gallery
## Types
| Type | Best For |
|------|----------|
| `infographic` | Data, metrics, technical articles |
| `scene` | Narratives, personal stories, emotional content |
| `flowchart` | Tutorials, workflows, processes |
| `comparison` | Side-by-side, before/after, options |
| `framework` | Methodologies, models, architecture |
| `timeline` | History, progress, evolution |
| `infographic` | Data, metrics, technical |
| `scene` | Narratives, emotional |
| `flowchart` | Processes, workflows |
| `comparison` | Side-by-side, options |
| `framework` | Models, architecture |
| `timeline` | History, evolution |
## Style Gallery
## Styles
| Style | Best For |
|-------|----------|
| `notion` (Default) | Knowledge sharing, SaaS, productivity |
| `elegant` | Business, thought leadership |
| `warm` | Personal growth, lifestyle, education |
| `minimal` | Philosophy, core concepts |
| `blueprint` | Architecture, system design |
| `watercolor` | Lifestyle, travel, creative |
| `editorial` | Tech explainers, journalism |
| `scientific` | Academic, technical research |
Full styles: [references/styles.md](references/styles.md)
## Auto Selection
| Content Signals | Type | Style |
|-----------------|------|-------|
| API, metrics, data, numbers | infographic | blueprint, notion |
| Story, emotion, journey | scene | warm, watercolor |
| How-to, steps, workflow | flowchart | notion, minimal |
| vs, pros/cons, before/after | comparison | notion, elegant |
| Framework, model, architecture | framework | blueprint, notion |
| History, timeline, progress | timeline | elegant, warm |
See [references/styles.md](references/styles.md) for Core Styles, full gallery, and Type × Style compatibility.
## Workflow
Copy this checklist and track progress:
```
Progress:
- [ ] Step 1: Pre-check
- [ ] Step 2: Setup & Analyze
- [ ] Step 3: Confirm Settings ⚠️ REQUIRED
- [ ] Step 4: Generate Outline
- [ ] Step 5: Generate Images
- [ ] Step 1: Pre-check (EXTEND.md, references, config)
- [ ] Step 2: Analyze content
- [ ] Step 3: Confirm settings (AskUserQuestion)
- [ ] Step 4: Generate outline
- [ ] Step 5: Generate images
- [ ] Step 6: Finalize
```
---
### Step 1: Pre-check
**1.1 Determine Input Type**
| Input | Output Directory | Next |
|-------|------------------|------|
| File path | Ask user (1.2) | → 1.2 |
| Pasted content | `illustrations/{topic-slug}/` | → 1.4 |
**1.2 Determine Output Directory** (file path input only)
Check `default_output_dir` in preferences:
| Preference Value | Action |
|------------------|--------|
| `same-dir` | Use `{article-dir}/`, display "Output: {path}" |
| `imgs-subdir` | Use `{article-dir}/imgs/`, display "Output: {path}" |
| `illustrations-subdir` | Use `{article-dir}/illustrations/`, display "Output: {path}" |
| `independent` | Use `illustrations/{topic-slug}/`, display "Output: {path}" |
| Not configured | **MUST** ask with AskUserQuestion ↓ |
**AskUserQuestion** (when no preference):
- `{article-dir}/` - Same directory as article
- `{article-dir}/imgs/` - Images subdirectory
- `{article-dir}/illustrations/` - Illustrations subdirectory (Recommended)
- `illustrations/{topic-slug}/` - Independent directory
- Save as default - Remember this choice for future runs
**1.3 Check Existing Images**
Scan target directory for `.png/.jpg/.webp` files.
If images exist → AskUserQuestion: How to handle?
- `supplement` - Keep existing, generate only new positions
- `overwrite` - Overwrite same-name files
- `regenerate` - Clear all and regenerate
**1.4 Confirm Article Update Method** (file path input only)
AskUserQuestion: How to update article?
- `update` - Modify original file directly
- `copy` - Create `{name}-illustrated.md` copy
**1.5 Load Preferences (EXTEND.md)**
**1.5 Load Preferences (EXTEND.md) ⛔ BLOCKING**
```bash
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-article-illustrator/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md") { "user" }
```
| Result | Action |
|--------|--------|
| Found | Read, parse, display summary |
| Not found | Ask with AskUserQuestion (see references/config/first-time-setup.md) |
| Not found | ⛔ Run [first-time-setup](references/config/first-time-setup.md) |
**Supports**: Watermark | Preferred type/style | Custom styles | Language | Output directory
Full procedures: [references/workflow.md](references/workflow.md#step-1-pre-check)
---
### Step 2: Analyze
### Step 2: Setup & Analyze
**2.1 Analyze Content**
| Analysis | Description |
|----------|-------------|
| Analysis | Output |
|----------|--------|
| Content type | Technical / Tutorial / Methodology / Narrative |
| Core arguments | 2-5 main points to visualize |
| Visual opportunities | Positions where illustrations add value |
| Recommended type | Based on content signals |
| Recommended density | Based on length and complexity |
| Purpose | information / visualization / imagination |
| Core arguments | 2-5 main points |
| Positions | Where illustrations add value |
**2.2 Extract Core Arguments**
**CRITICAL**: Metaphors → visualize underlying concept, NOT literal image.
- Main thesis
- Key concepts reader needs
- Comparisons/contrasts
- Framework/model proposed
**CRITICAL**: If article uses metaphors (e.g., "电锯切西瓜"), do NOT illustrate literally. Visualize the **underlying concept**.
**2.3 Identify Positions**
**Illustrate**:
- Core arguments (REQUIRED)
- Abstract concepts
- Data comparisons
- Processes, workflows
**Do NOT Illustrate**:
- Metaphors literally
- Decorative scenes
- Generic illustrations
---
Full procedures: [references/workflow.md](references/workflow.md#step-2-setup--analyze)
### Step 3: Confirm Settings ⚠️
**Do NOT skip.** Use AskUserQuestion with 3-4 questions in ONE call.
**ONE AskUserQuestion, max 4 Qs. Q1-Q3 REQUIRED.**
**Q1: Illustration Type**
- [Recommended based on analysis] (Recommended)
- infographic / scene / flowchart / comparison / framework / timeline / mixed
| Q | Options |
|---|---------|
| **Q1: Type** | [Recommended], infographic, scene, flowchart, comparison, framework, timeline, mixed |
| **Q2: Density** | minimal (1-2), balanced (3-5), per-section (Recommended), rich (6+) |
| **Q3: Style** | [Recommended], minimal-flat, sci-fi, hand-drawn, editorial, scene, Other |
| Q4: Language | When article language ≠ EXTEND.md setting |
**Q2: Density**
- minimal (1-2) - Core concepts only
- balanced (3-5) (Recommended) - Major sections
- rich (6+) - Comprehensive support
**Q3: Style** (ALWAYS ask, even with preferred_style in EXTEND.md)
If EXTEND.md has `preferred_style`:
- [Custom style name + brief description] (Recommended)
- [Top compatible built-in style 1]
- [Top compatible built-in style 2]
- [Top compatible built-in style 3]
If no `preferred_style`:
- [Best compatible from matrix] (Recommended)
- [Other ✓✓ style 1]
- [Other ✓✓ style 2]
- [Other ✓ style]
Style selection based on Type × Style compatibility matrix (references/styles.md).
Full specs: `references/styles/<style>.md`
**Q4** (only if source ≠ user language):
- Language: Source / User language
---
Full procedures: [references/workflow.md](references/workflow.md#step-3-confirm-settings-)
### Step 4: Generate Outline
Save as `outline.md`:
Save `outline.md` with frontmatter (type, density, style, image_count) and entries:
```yaml
---
type: infographic
density: balanced
style: blueprint
image_count: 4
---
## Illustration 1
**Position**: [section] / [paragraph]
**Purpose**: [why this helps]
**Visual Content**: [what to show]
**Type Application**: [how type applies]
**Position**: [section/paragraph]
**Purpose**: [why]
**Visual Content**: [what]
**Filename**: 01-infographic-concept-name.png
## Illustration 2
...
```
**Requirements**:
- Each position justified by content needs
- Type applied consistently
- Style reflected in descriptions
- Count matches density
---
Full template: [references/workflow.md](references/workflow.md#step-4-generate-outline)
### Step 5: Generate Images
**5.1 Create Prompts**
**BLOCKING: Prompt files MUST be saved before ANY image generation.**
Follow [references/prompt-construction.md](references/prompt-construction.md). Save to `prompts/illustration-{slug}.md`.
1. For each illustration, create a prompt file per [references/prompt-construction.md](references/prompt-construction.md)
2. Save to `prompts/NN-{type}-{slug}.md` with YAML frontmatter
3. Prompts **MUST** use type-specific templates with structured sections (ZONES / LABELS / COLORS / STYLE / ASPECT)
4. LABELS **MUST** include article-specific data: actual numbers, terms, metrics, quotes
5. **DO NOT** pass ad-hoc inline prompts to `--prompt` without saving prompt files first
6. Select generation skill, process references (`direct`/`style`/`palette`)
7. Apply watermark if EXTEND.md enabled
8. Generate from saved prompt files; retry once on failure
**5.2 Select Generation Skill**
Check available skills. If multiple, ask user.
**5.3 Apply Watermark** (if enabled)
Add: `Include a subtle watermark "[content]" at [position].`
**5.4 Generate**
1. Generate sequentially
2. After each: "Generated X/N"
3. On failure: retry once, then log and continue
---
Full procedures: [references/workflow.md](references/workflow.md#step-5-generate-images)
### Step 6: Finalize
**6.1 Update Article**
Insert after corresponding paragraph:
```markdown
![description](illustrations/{slug}/NN-{type}-{slug}.png)
```
Alt text: concise description in article's language.
**6.2 Output Summary**
Insert `![description](path/NN-{type}-{slug}.png)` after paragraphs.
```
Article Illustration Complete!
Article: [path]
Type: [type] | Density: [level] | Style: [style]
Location: [directory]
Article: [path] | Type: [type] | Density: [level] | Style: [style]
Images: X/N generated
Positions:
- 01-xxx.png → After "[Section]"
- 02-yyy.png → After "[Section]"
[If failures]
Failed:
- NN-zzz.png: [reason]
```
---
## Output Directory
```
illustrations/{topic-slug}/
├── source-{slug}.{ext}
├── references/ # if provided
├── outline.md
├── prompts/
│ └── illustration-{slug}.md
└── NN-{type}-{slug}.png
```
**Slug**: 2-4 word topic in kebab-case.
**Conflict**: Append `-YYYYMMDD-HHMMSS` if exists.
**Slug**: 2-4 words, kebab-case. **Conflict**: append `-YYYYMMDD-HHMMSS`.
## Modification
| Action | Steps |
|--------|-------|
| **Edit** | **Update prompt file FIRST** → Regenerate → Update reference |
| **Add** | Identify position → Create prompt → Generate → Update outline → Insert |
| **Delete** | Delete files → Remove reference → Update outline |
**IMPORTANT**: When updating illustrations, ALWAYS update the prompt file (`prompts/illustration-{slug}.md`) FIRST before regenerating. This ensures:
1. Changes are documented and reproducible
2. The prompt reflects the new requirements
3. Future regeneration uses the correct prompt
| Edit | Update prompt → Regenerate → Update reference |
| Add | Position → Prompt → Generate → Update outline → Insert |
| Delete | Delete files → Remove reference → Update outline |
## References
| File | Content |
|------|---------|
| [references/usage.md](references/usage.md) | Command syntax and options |
| [references/styles.md](references/styles.md) | Style gallery & compatibility |
| [references/workflow.md](references/workflow.md) | Detailed procedures |
| [references/usage.md](references/usage.md) | Command syntax |
| [references/styles.md](references/styles.md) | Style gallery |
| [references/prompt-construction.md](references/prompt-construction.md) | Prompt templates |
| `references/styles/<style>.md` | Full style specifications |
| `references/config/preferences-schema.md` | EXTEND.md schema |
| `references/config/first-time-setup.md` | First-time setup flow |
## Extension Support
Custom configurations via EXTEND.md. See **Step 1.5** for paths and supported options.
| [references/config/first-time-setup.md](references/config/first-time-setup.md) | First-time setup |
@@ -9,6 +9,14 @@ description: First-time setup flow for baoyu-article-illustrator preferences
When no EXTEND.md is found, guide user through preference setup.
**⛔ BLOCKING OPERATION**: This setup MUST complete before ANY other workflow steps. Do NOT:
- Ask about reference images
- Ask about content/article
- Ask about type or style preferences
- Proceed to content analysis
ONLY ask the questions in this setup flow, save EXTEND.md, then continue.
## Setup Flow
```
@@ -1,5 +1,102 @@
# Prompt Construction
## Prompt File Format
Each prompt file uses YAML frontmatter + content:
```yaml
---
illustration_id: 01
type: infographic
style: blueprint
references: # ⚠️ ONLY if files EXIST in references/ directory
- ref_id: 01
filename: 01-ref-diagram.png
usage: direct # direct | style | palette
---
[Type-specific template content below...]
```
**⚠️ CRITICAL - When to include `references` field**:
| Situation | Action |
|-----------|--------|
| Reference file saved to `references/` | Include in frontmatter ✓ |
| Style extracted verbally (no file) | DO NOT include in frontmatter, append to prompt body instead |
| File path in frontmatter but file doesn't exist | ERROR - remove references field |
**Reference Usage Types** (only when file exists):
| Usage | Description | Generation Action |
|-------|-------------|-------------------|
| `direct` | Primary visual reference | Pass to `--ref` parameter |
| `style` | Style characteristics only | Describe style in prompt text |
| `palette` | Color palette extraction | Include colors in prompt |
**If no reference file but style/palette extracted verbally**, append directly to prompt body:
```
COLORS (from reference):
- Primary: #E8756D coral
- Secondary: #7ECFC0 mint
...
STYLE (from reference):
- Clean lines, minimal shadows
- Gradient backgrounds
...
```
---
## Default Composition Requirements
**Apply to ALL prompts by default**:
| Requirement | Description |
|-------------|-------------|
| **Clean composition** | Simple layouts, no visual clutter |
| **White space** | Generous margins, breathing room around elements |
| **No complex backgrounds** | Solid colors or subtle gradients only, avoid busy textures |
| **Centered or content-appropriate** | Main visual elements centered or positioned by content needs |
| **Matching graphics** | Use graphic elements that align with content theme |
| **Highlight core info** | White space draws attention to key information |
**Add to ALL prompts**:
> Clean composition with generous white space. Simple or no background. Main elements centered or positioned by content needs.
---
## Character Rendering
When depicting people:
| Guideline | Description |
|-----------|-------------|
| **Style** | Simplified cartoon silhouettes or symbolic expressions |
| **Avoid** | Realistic human portrayals, detailed faces |
| **Diversity** | Varied body types when showing multiple people |
| **Emotion** | Express through posture and simple gestures |
**Add to ALL prompts with human figures**:
> Human figures: simplified stylized silhouettes or symbolic representations, not photorealistic.
---
## Text in Illustrations
| Element | Guideline |
|---------|-----------|
| **Size** | Large, prominent, immediately readable |
| **Style** | Handwritten fonts preferred for warmth |
| **Content** | Concise keywords and core concepts only |
| **Language** | Match article language |
**Add to prompts with text**:
> Text should be large and prominent with handwritten-style fonts. Keep minimal, focus on keywords.
---
## Principles
Good prompts must include:
@@ -31,6 +128,13 @@ STYLE: [style characteristics]
ASPECT: 16:9
```
**Infographic + vector-illustration**:
```
Flat vector illustration infographic. Clean black outlines on all elements.
COLORS: Cream background (#F5F0E6), Coral Red (#E07A5F), Mint Green (#81B29A), Mustard Yellow (#F2CC8F)
ELEMENTS: Geometric simplified icons, no gradients, playful decorative elements (dots, stars)
```
### Scene
```
@@ -61,6 +165,13 @@ STYLE: [style characteristics]
ASPECT: 16:9
```
**Flowchart + vector-illustration**:
```
Flat vector flowchart with bold arrows and geometric step containers.
COLORS: Cream background (#F5F0E6), steps in Coral/Mint/Mustard, black outlines
ELEMENTS: Rounded rectangles, thick arrows, simple icons per step
```
### Comparison
```
@@ -79,6 +190,13 @@ STYLE: [style characteristics]
ASPECT: 16:9
```
**Comparison + vector-illustration**:
```
Flat vector comparison with split layout. Clear visual separation.
COLORS: Left side Coral (#E07A5F), Right side Mint (#81B29A), cream background
ELEMENTS: Bold icons, black outlines, centered divider line
```
### Framework
```
@@ -95,6 +213,13 @@ STYLE: [style characteristics]
ASPECT: 16:9
```
**Framework + vector-illustration**:
```
Flat vector framework diagram with geometric nodes and bold connectors.
COLORS: Cream background (#F5F0E6), nodes in Coral/Mint/Mustard/Blue, black outlines
ELEMENTS: Rounded rectangles or circles for nodes, thick connecting lines
```
### Timeline
```
@@ -1,10 +1,28 @@
# Style Reference
## Core Styles
Simplified style tier for quick selection:
| Core Style | Maps To | Best For |
|------------|---------|----------|
| `vector` | vector-illustration | Knowledge articles, tutorials, tech content |
| `minimal-flat` | notion | General, knowledge sharing, SaaS |
| `sci-fi` | blueprint | AI, frontier tech, system design |
| `hand-drawn` | sketch/warm | Relaxed, reflective, casual content |
| `editorial` | editorial | Processes, data, journalism |
| `scene` | warm/watercolor | Narratives, emotional, lifestyle |
Use Core Styles for most cases. See full Style Gallery below for granular control.
---
## Style Gallery
| Style | Description | Best For |
|-------|-------------|----------|
| `notion` (Default) | Minimalist hand-drawn line art | Knowledge sharing, SaaS, productivity |
| `vector-illustration` | Clean flat vector art with bold shapes | Knowledge articles, tutorials, tech content |
| `notion` | Minimalist hand-drawn line art | Knowledge sharing, SaaS, productivity |
| `elegant` | Refined, sophisticated | Business, thought leadership |
| `warm` | Friendly, approachable | Personal growth, lifestyle, education |
| `minimal` | Ultra-clean, zen-like | Philosophy, minimalism, core concepts |
@@ -12,19 +30,31 @@
| `watercolor` | Soft artistic with natural warmth | Lifestyle, travel, creative |
| `editorial` | Magazine-style infographic | Tech explainers, journalism |
| `scientific` | Academic precise diagrams | Biology, chemistry, technical research |
| `chalkboard` | Classroom chalk drawing style | Education, teaching, explanations |
| `fantasy-animation` | Ghibli/Disney-inspired hand-drawn | Storybook, magical, emotional |
| `flat` | Modern bold geometric shapes | Modern digital, contemporary |
| `flat-doodle` | Cute flat with bold outlines | Cute, friendly, approachable |
| `intuition-machine` | Technical briefing with aged paper | Technical briefings, academic |
| `nature` | Organic earthy illustration | Environmental, wellness |
| `pixel-art` | Retro 8-bit gaming aesthetic | Gaming, retro tech |
| `playful` | Whimsical pastel doodles | Fun, casual, educational |
| `retro` | 80s/90s neon geometric | 80s/90s nostalgic, bold |
| `sketch` | Raw pencil notebook style | Brainstorming, creative exploration |
| `sketch-notes` | Soft hand-drawn warm notes | Educational, warm notes |
| `vintage` | Aged parchment historical | Historical, heritage |
Full specifications: `references/styles/<style>.md`
## Type × Style Compatibility Matrix
| | notion | warm | minimal | blueprint | watercolor | elegant | editorial | scientific |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| infographic | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓✓ |
| scene | ✓ | ✓✓ | ✓ | ✗ | ✓✓ | ✓ | ✓ | ✗ |
| flowchart | ✓✓ | ✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ |
| comparison | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
| framework | ✓✓ | ✓ | ✓✓ | ✓✓ | ✗ | ✓✓ | ✓ | ✓✓ |
| timeline | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ |
| | vector-illustration | notion | warm | minimal | blueprint | watercolor | elegant | editorial | scientific |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| infographic | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓✓ |
| scene | ✓ | ✓ | ✓✓ | ✓ | ✗ | ✓✓ | ✓ | ✓ | ✗ |
| flowchart | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ |
| comparison | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
| framework | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✗ | ✓✓ | ✓ | ✓✓ |
| timeline | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ |
✓✓ = highly recommended | ✓ = compatible | ✗ = not recommended
@@ -32,30 +62,57 @@ Full specifications: `references/styles/<style>.md`
| Type | Primary Style | Secondary Styles |
|------|---------------|------------------|
| infographic | blueprint | notion, editorial, scientific |
| infographic | vector-illustration | notion, blueprint, editorial |
| scene | warm | watercolor, elegant |
| flowchart | notion | blueprint, editorial |
| comparison | notion | elegant, editorial |
| framework | blueprint | notion, scientific |
| flowchart | vector-illustration | notion, blueprint |
| comparison | vector-illustration | notion, elegant |
| framework | blueprint | vector-illustration, notion |
| timeline | elegant | warm, editorial |
## Auto Selection by Content Signals
| Content Signals | Recommended Type | Recommended Style |
|-----------------|------------------|-------------------|
| API, metrics, data, comparison, numbers | infographic | blueprint, notion |
| API, metrics, data, comparison, numbers | infographic | blueprint, vector-illustration |
| Knowledge, concept, tutorial, learning, guide | infographic | vector-illustration, notion |
| Tech, AI, programming, development, code | infographic | vector-illustration, blueprint |
| How-to, steps, workflow, process, tutorial | flowchart | vector-illustration, notion |
| Framework, model, architecture, principles | framework | blueprint, vector-illustration |
| vs, pros/cons, before/after, alternatives | comparison | vector-illustration, notion |
| Story, emotion, journey, experience, personal | scene | warm, watercolor |
| How-to, steps, workflow, process, tutorial | flowchart | notion, minimal |
| vs, pros/cons, before/after, alternatives | comparison | notion, elegant |
| Framework, model, architecture, principles | framework | blueprint, notion |
| History, timeline, progress, evolution | timeline | elegant, warm |
| Knowledge, concept, productivity, SaaS, tool | infographic | notion |
| Productivity, SaaS, tool, app, software | infographic | notion, vector-illustration |
| Business, professional, strategy, corporate | framework | elegant |
| Biology, chemistry, medical, scientific | infographic | scientific |
| Explainer, journalism, magazine, investigation | infographic | editorial |
## Style Characteristics by Type
### infographic + vector-illustration
- Clean flat vector shapes, bold geometric forms
- Vibrant but harmonious color palette
- Clear visual hierarchy with icons and labels
- Modern, professional, highly readable
- Perfect for knowledge articles and tutorials
### flowchart + vector-illustration
- Bold arrows and connectors
- Distinct step containers with icons
- Clean progression flow
- High contrast for readability
### comparison + vector-illustration
- Split layout with clear visual separation
- Bold iconography for each side
- Color-coded distinctions
- Easy at-a-glance comparison
### framework + vector-illustration
- Geometric node representations
- Clear hierarchical structure
- Bold connecting lines
- Modern system diagram aesthetic
### infographic + blueprint
- Technical precision, schematic lines
- Grid-based layout, clear zones
@@ -0,0 +1,393 @@
# Detailed Workflow Procedures
## Step 1: Pre-check
### 1.0 Detect & Save Reference Images ⚠️ REQUIRED if images provided
Check if user provided reference images. Handle based on input type:
| Input Type | Action |
|------------|--------|
| Image file path provided | Copy to `references/` subdirectory → can use `--ref` |
| Image in conversation (no path) | **ASK user for file path** with AskUserQuestion |
| User can't provide path | Extract style/palette verbally → append to prompts (NO frontmatter references) |
**CRITICAL**: Only add `references` to prompt frontmatter if files are ACTUALLY SAVED to `references/` directory.
**If user provides file path**:
1. Copy to `references/NN-ref-{slug}.png`
2. Create description: `references/NN-ref-{slug}.md`
3. Verify files exist before proceeding
**If user can't provide path** (extracted verbally):
1. Analyze image visually, extract: colors, style, composition
2. Create `references/extracted-style.md` with extracted info
3. DO NOT add `references` to prompt frontmatter
4. Instead, append extracted style/colors directly to prompt text
**Description File Format** (only when file saved):
```yaml
---
ref_id: NN
filename: NN-ref-{slug}.png
---
[User's description or auto-generated description]
```
**Verification** (only for saved files):
```
Reference Images Saved:
- 01-ref-{slug}.png ✓ (can use --ref)
- 02-ref-{slug}.png ✓ (can use --ref)
```
**Or for extracted style**:
```
Reference Style Extracted (no file):
- Colors: #E8756D coral, #7ECFC0 mint...
- Style: minimal flat vector, clean lines...
→ Will append to prompt text (not --ref)
```
---
### 1.1 Determine Input Type
| Input | Output Directory | Next |
|-------|------------------|------|
| File path | Ask user (1.2) | → 1.2 |
| Pasted content | `illustrations/{topic-slug}/` | → 1.4 |
**Backup rule for pasted content**: If `source.md` exists in target directory, rename to `source-backup-YYYYMMDD-HHMMSS.md` before saving.
### 1.2-1.4 Configuration (file path input only)
Check preferences and existing state, then ask ALL needed questions in ONE AskUserQuestion call (max 4 questions).
**Questions to include** (skip if preference exists or not applicable):
| Question | When to Ask | Options |
|----------|-------------|---------|
| Output directory | No `default_output_dir` in EXTEND.md | `{article-dir}/`, `{article-dir}/imgs/` (Recommended), `{article-dir}/illustrations/`, `illustrations/{topic-slug}/` |
| Existing images | Target dir has `.png/.jpg/.webp` files | `supplement`, `overwrite`, `regenerate` |
| Article update | Always (file path input) | `update`, `copy` |
**Preference Values** (if configured, skip asking):
| `default_output_dir` | Path |
|----------------------|------|
| `same-dir` | `{article-dir}/` |
| `imgs-subdir` | `{article-dir}/imgs/` |
| `illustrations-subdir` | `{article-dir}/illustrations/` |
| `independent` | `illustrations/{topic-slug}/` |
### 1.5 Load Preferences (EXTEND.md) ⛔ BLOCKING
**CRITICAL**: If EXTEND.md not found, MUST complete first-time setup before ANY other questions or steps. Do NOT proceed to reference images, do NOT ask about content, do NOT ask about type/style — ONLY complete the preferences setup first.
```bash
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-article-illustrator/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md") { "user" }
```
| Result | Action |
|--------|--------|
| Found | Read, parse, display summary → Continue |
| Not found | ⛔ **BLOCKING**: Run first-time setup ONLY ([config/first-time-setup.md](config/first-time-setup.md)) → Complete and save EXTEND.md → Then continue |
**Supports**: Watermark | Preferred type/style | Custom styles | Language | Output directory
---
## Step 2: Setup & Analyze
### 2.1 Analyze Content
| Analysis | Description |
|----------|-------------|
| Content type | Technical / Tutorial / Methodology / Narrative |
| Illustration purpose | information / visualization / imagination |
| Core arguments | 2-5 main points to visualize |
| Visual opportunities | Positions where illustrations add value |
| Recommended type | Based on content signals and purpose |
| Recommended density | Based on length and complexity |
### 2.2 Extract Core Arguments
- Main thesis
- Key concepts reader needs
- Comparisons/contrasts
- Framework/model proposed
**CRITICAL**: If article uses metaphors (e.g., "电锯切西瓜"), do NOT illustrate literally. Visualize the **underlying concept**.
### 2.3 Identify Positions
**Illustrate**:
- Core arguments (REQUIRED)
- Abstract concepts
- Data comparisons
- Processes, workflows
**Do NOT Illustrate**:
- Metaphors literally
- Decorative scenes
- Generic illustrations
### 2.4 Analyze Reference Images (if provided in Step 1.0)
For each reference image:
| Analysis | Description |
|----------|-------------|
| Visual characteristics | Style, colors, composition |
| Content/subject | What the reference depicts |
| Suitable positions | Which sections match this reference |
| Style match | Which illustration types/styles align |
| Usage recommendation | `direct` / `style` / `palette` |
| Usage | When to Use |
|-------|-------------|
| `direct` | Reference matches desired output closely |
| `style` | Extract visual style characteristics only |
| `palette` | Extract color scheme only |
---
## Step 3: Confirm Settings ⚠️
**Do NOT skip.** Use ONE AskUserQuestion call with max 4 questions. **Q1, Q2, Q3 are ALL REQUIRED.**
### Q1: Illustration Type ⚠️ REQUIRED
- [Recommended based on analysis] (Recommended)
- infographic / scene / flowchart / comparison / framework / timeline / mixed
### Q2: Density ⚠️ REQUIRED - DO NOT SKIP
- minimal (1-2) - Core concepts only
- balanced (3-5) - Major sections
- per-section - At least 1 per section/chapter (Recommended)
- rich (6+) - Comprehensive coverage
### Q3: Style ⚠️ REQUIRED (ALWAYS ask, even with preferred_style in EXTEND.md)
If EXTEND.md has `preferred_style`:
- [Custom style name + brief description] (Recommended)
- [Top compatible core style 1]
- [Top compatible core style 2]
- Other (see full Style Gallery)
If no `preferred_style` (present Core Styles first):
- [Best compatible core style] (Recommended)
- [Other compatible core style 1]
- [Other compatible core style 2]
- Other (see full Style Gallery)
**Core Styles** (simplified selection):
| Core Style | Best For |
|------------|----------|
| `minimal-flat` | General, knowledge sharing, SaaS |
| `sci-fi` | AI, frontier tech, system design |
| `hand-drawn` | Relaxed, reflective, casual |
| `editorial` | Processes, data, journalism |
| `scene` | Narratives, emotional, lifestyle |
Style selection based on Type × Style compatibility matrix (styles.md).
Full specs: `styles/<style>.md`
### Q4: Image Text Language ⚠️ REQUIRED when article language ≠ EXTEND.md `language`
Detect article language from content. If different from EXTEND.md `language` setting, MUST ask:
- Article language (match article content) (Recommended)
- EXTEND.md language (user's general preference)
**Skip only if**: Article language matches EXTEND.md `language`, or EXTEND.md has no `language` setting.
### Display Reference Usage (if references detected in Step 1.0)
When presenting outline preview to user, show reference assignments:
```
Reference Images:
| Ref | Filename | Recommended Usage |
|-----|----------|-------------------|
| 01 | 01-ref-diagram.png | direct → Illustration 1, 3 |
| 02 | 02-ref-chart.png | palette → Illustration 2 |
```
---
## Step 4: Generate Outline
Save as `outline.md`:
```yaml
---
type: infographic
density: balanced
style: blueprint
image_count: 4
references: # Only if references provided
- ref_id: 01
filename: 01-ref-diagram.png
description: "Technical diagram showing system architecture"
- ref_id: 02
filename: 02-ref-chart.png
description: "Color chart with brand palette"
---
## Illustration 1
**Position**: [section] / [paragraph]
**Purpose**: [why this helps]
**Visual Content**: [what to show]
**Type Application**: [how type applies]
**References**: [01] # Optional: list ref_ids used
**Reference Usage**: direct # direct | style | palette
**Filename**: 01-infographic-concept-name.png
## Illustration 2
...
```
**Requirements**:
- Each position justified by content needs
- Type applied consistently
- Style reflected in descriptions
- Count matches density
- References assigned based on Step 2.4 analysis
---
## Step 5: Generate Images
### 5.1 Create Prompts ⛔ BLOCKING
**Every illustration MUST have a saved prompt file before generation begins. DO NOT skip this step.**
For each illustration in the outline:
1. **Create prompt file**: `prompts/NN-{type}-{slug}.md`
2. **Include YAML frontmatter**:
```yaml
---
illustration_id: 01
type: infographic
style: custom-flat-vector
---
```
3. **Follow type-specific template** from [prompt-construction.md](prompt-construction.md)
4. **Prompt quality requirements** (all REQUIRED):
- `Layout`: Describe overall composition (grid / radial / hierarchical / left-right / top-down)
- `ZONES`: Describe each visual area with specific content, not vague descriptions
- `LABELS`: Use **actual numbers, terms, metrics, quotes from the article** — NOT generic placeholders
- `COLORS`: Specify hex codes with semantic meaning (e.g., `Coral (#E07A5F) for emphasis`)
- `STYLE`: Describe line treatment, texture, mood, character rendering
- `ASPECT`: Specify ratio (e.g., `16:9`)
5. **Apply defaults**: composition requirements, character rendering, text guidelines, watermark
6. **Backup rule**: If prompt file exists, rename to `prompts/NN-{type}-{slug}-backup-YYYYMMDD-HHMMSS.md`
**Verification** ⛔: Before proceeding to 5.2, confirm ALL prompt files exist:
```
Prompt Files:
- prompts/01-infographic-overview.md ✓
- prompts/02-infographic-distillation.md ✓
...
```
**DO NOT** pass ad-hoc inline text to `--prompt` without first saving prompt files. The generation command should either use `--promptfiles prompts/NN-{type}-{slug}.md` or read the saved file content for `--prompt`.
**CRITICAL - References in Frontmatter**:
- Only add `references` field if files ACTUALLY EXIST in `references/` directory
- If style/palette was extracted verbally (no file), append info to prompt BODY instead
- Before writing frontmatter, verify: `test -f references/NN-ref-{slug}.png`
### 5.2 Select Generation Skill
Check available skills. If multiple, ask user.
### 5.3 Process References ⚠️ REQUIRED if references saved in Step 1.0
**DO NOT SKIP if user provided reference images.** For each illustration with references:
1. **VERIFY files exist first**:
```bash
test -f references/NN-ref-{slug}.png && echo "exists" || echo "MISSING"
```
- If file MISSING but in frontmatter → ERROR, fix frontmatter or remove references field
- If file exists → proceed with processing
2. Read prompt frontmatter for reference info
3. Process based on usage type:
| Usage | Action | Example |
|-------|--------|---------|
| `direct` | Add reference path to `--ref` parameter | `--ref references/01-ref-brand.png` |
| `style` | Analyze reference, append style traits to prompt | "Style: clean lines, gradient backgrounds..." |
| `palette` | Extract colors from reference, append to prompt | "Colors: #E8756D coral, #7ECFC0 mint..." |
4. Check image generation skill capability:
| Skill Supports `--ref` | Action |
|------------------------|--------|
| Yes (e.g., baoyu-image-gen with Google) | Pass reference images via `--ref` |
| No | Convert to text description, append to prompt |
**Verification**: Before generating, confirm reference processing:
```
Reference Processing:
- Illustration 1: using 01-ref-brand.png (direct) ✓
- Illustration 2: extracted palette from 02-ref-style.png ✓
```
### 5.4 Apply Watermark (if enabled)
Add: `Include a subtle watermark "[content]" at [position].`
### 5.5 Generate
1. For each illustration:
- **Backup rule**: If image file exists, rename to `NN-{type}-{slug}-backup-YYYYMMDD-HHMMSS.md`
- If references with `direct` usage: include `--ref` parameter
- Generate image
2. After each: "Generated X/N"
3. On failure: retry once, then log and continue
---
## Step 6: Finalize
### 6.1 Update Article
Insert after corresponding paragraph:
```markdown
![description](illustrations/{slug}/NN-{type}-{slug}.png)
```
Alt text: concise description in article's language.
### 6.2 Output Summary
```
Article Illustration Complete!
Article: [path]
Type: [type] | Density: [level] | Style: [style]
Location: [directory]
Images: X/N generated
Positions:
- 01-xxx.png → After "[Section]"
- 02-yyy.png → After "[Section]"
[If failures]
Failed:
- NN-zzz.png: [reason]
```
+31 -6
View File
@@ -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`
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
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 | Purpose |
@@ -153,7 +154,11 @@ Technical terms remain in English.
```
Comic Progress:
- [ ] Step 1: Setup & Analyze (1.1 Preferences, 1.2 Analyze, 1.3 Check existing)
- [ ] Step 1: Setup & Analyze
- [ ] 1.1 Preferences (EXTEND.md) ⛔ BLOCKING
- [ ] Found → load preferences → continue
- [ ] Not found → run first-time setup → MUST complete before other steps
- [ ] 1.2 Analyze, 1.3 Check existing
- [ ] Step 2: Confirmation - Style & options ⚠️ REQUIRED
- [ ] Step 3: Generate storyboard + characters
- [ ] Step 4: Review outline (conditional)
@@ -169,14 +174,22 @@ Comic Progress:
### Flow
```
Input → Preferences → Analyze → [Check Existing?] → [Confirm: Style + Reviews] → Storyboard → [Review?] → Prompts → [Review?] → Images → PDF → Complete
Input → [Preferences] ─┬─ Found → Continue
└─ Not found → First-Time Setup ⛔ BLOCKING
└─ Complete setup → Save EXTEND.md → Continue
┌─────────────────────────────────────────────────────────────────────┘
Analyze → [Check Existing?] → [Confirm: Style + Reviews] → Storyboard → [Review?] → Prompts → [Review?] → Images → PDF → Complete
```
### Step Summary
| Step | Action | Key Output |
|------|--------|------------|
| 1.1 | Load EXTEND.md preferences | Config loaded |
| 1.1 | Load EXTEND.md preferences ⛔ BLOCKING if not found | Config loaded |
| 1.2 | Analyze content | `analysis.md` |
| 1.3 | Check existing directory | Handle conflicts |
| 2 | Confirm style, focus, audience, reviews | User preferences |
@@ -194,9 +207,10 @@ Input → Preferences → Analyze → [Check Existing?] → [Confirm: Style + Re
**Character reference is MANDATORY for visual consistency.**
**7.1 Generate character sheet first**:
- **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
```bash
# 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 \
--image characters/characters.png --ar 4:3
```
@@ -214,9 +228,13 @@ Compress to reduce token usage when used as reference image:
| Supports `--ref` | Pass `characters/characters.png` with EVERY page |
| No `--ref` support | Prepend character descriptions to EVERY prompt file |
**Backup rules for page generation**:
- If prompt file exists: rename to `prompts/NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.md`
- If image file exists: rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
```bash
# 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 \
--image 01-page-xxx.png --ar 3:4 \
--ref characters/characters.png
@@ -224,13 +242,20 @@ npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
**Full workflow details**: [references/workflow.md](references/workflow.md)
### EXTEND.md Paths
### EXTEND.md Paths ⛔ BLOCKING
**CRITICAL**: If EXTEND.md not found, MUST complete first-time setup before ANY other questions or steps. Do NOT proceed to content analysis, do NOT ask about art style, do NOT ask about tone — ONLY complete the preferences setup first.
| Path | Location |
|------|----------|
| `.baoyu-skills/baoyu-comic/EXTEND.md` | Project directory |
| `$HOME/.baoyu-skills/baoyu-comic/EXTEND.md` | User home |
| Result | Action |
|--------|--------|
| Found | Read, parse, display summary → Continue |
| Not found | ⛔ **BLOCKING**: Run first-time setup ONLY ([references/config/first-time-setup.md](references/config/first-time-setup.md)) → Complete and save EXTEND.md → Then continue |
**EXTEND.md Supports**: Watermark | Preferred art/tone/layout | Custom style definitions | Character presets | Language preference
Schema: [references/config/preferences-schema.md](references/config/preferences-schema.md)
@@ -9,6 +9,14 @@ description: First-time setup flow for baoyu-comic preferences
When no EXTEND.md is found, guide user through preference setup.
**⛔ BLOCKING OPERATION**: This setup MUST complete before ANY other workflow steps. Do NOT:
- Ask about content/source material
- Ask about art style or tone
- Ask about layout preferences
- Proceed to content analysis
ONLY ask the questions in this setup flow, save EXTEND.md, then continue.
## Setup Flow
```
+19 -11
View File
@@ -34,16 +34,20 @@ Input → Preferences → Analyze → [Check Existing?] → [Confirm 1: Style +
### 1.1 Load Preferences (EXTEND.md)
Use Bash to check EXTEND.md existence (priority order):
Check EXTEND.md existence (priority order):
```bash
# Check project-level first
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-comic/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-comic/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-comic/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-comic/EXTEND.md") { "user" }
```
| Path | Location |
|------|----------|
| `.baoyu-skills/baoyu-comic/EXTEND.md` | Project directory |
@@ -84,6 +88,7 @@ Read source content, save it if needed, and perform deep analysis.
1. **Save source content** (if not already a file):
- If user provides a file path: use as-is
- If user pastes content: save to `source.md` in target directory
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
2. Read source content
3. **Deep analysis** following `analysis-framework.md`:
- Target audience identification
@@ -329,6 +334,7 @@ Create image generation prompts for all pages.
1. Create prompt following art style + tone guidelines
2. Include character visual descriptions for consistency
3. Save to `prompts/NN-{cover|page}-[slug].md`
- **Backup rule**: If prompt file exists, rename to `prompts/NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.md`
**Prompt File Format**:
```markdown
@@ -405,8 +411,9 @@ With confirmed prompts from Step 5/6:
### 7.1 Generate Character Reference Sheet (first)
1. Use Reference Sheet Prompt from `characters/characters.md`
2. Generate → `characters/characters.png`
3. This ensures visual consistency for all subsequent pages
2. **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
3. Generate → `characters/characters.png`
4. This ensures visual consistency for all subsequent pages
### 7.2 Generate Comic Pages
@@ -428,7 +435,7 @@ With confirmed prompts from Step 5/6:
```bash
# 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 \
--image 01-page-xxx.png \
--ar 3:4 \
@@ -453,9 +460,10 @@ When skill does NOT support reference images, create combined prompt files:
**For each page (cover + pages)**:
1. Read prompt from `prompts/NN-{cover|page}-[slug].md`
2. Generate image using Strategy A or B (based on skill capability)
3. Save to `NN-{cover|page}-[slug].png`
4. Report progress after each generation: "Generated X/N: [page title]"
2. **Backup rule**: If image file exists, rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
3. Generate image using Strategy A or B (based on skill capability)
4. Save to `NN-{cover|page}-[slug].png`
5. Report progress after each generation: "Generated X/N: [page title]"
**Session Management**:
If image generation skill supports `--sessionId`:
@@ -470,7 +478,7 @@ If image generation skill supports `--sessionId`:
After all images generated:
```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.
+14 -10
View File
@@ -9,7 +9,7 @@ Compresses images using best available tool (sips → cwebp → ImageMagick →
## 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 |
|--------|---------|
@@ -17,16 +17,20 @@ Scripts in `scripts/` subdirectory. Replace `${SKILL_DIR}` with this SKILL.md's
## Preferences (EXTEND.md)
Use Bash to check EXTEND.md existence (priority order):
Check EXTEND.md existence (priority order):
```bash
# Check project-level first
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-compress-image/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-compress-image/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-compress-image/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-compress-image/EXTEND.md") { "user" }
```
┌────────────────────────────────────────────────────────┬───────────────────┐
│ Path │ Location │
├────────────────────────────────────────────────────────┼───────────────────┤
@@ -48,7 +52,7 @@ test -f "$HOME/.baoyu-skills/baoyu-compress-image/EXTEND.md" && echo "user"
## Usage
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts <input> [options]
${BUN_X} ${SKILL_DIR}/scripts/main.ts <input> [options]
```
## Options
@@ -67,16 +71,16 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts <input> [options]
```bash
# 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
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
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
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png --json
${BUN_X} ${SKILL_DIR}/scripts/main.ts image.png --json
```
**Output**:
+113 -163
View File
@@ -1,6 +1,6 @@
---
name: baoyu-cover-image
description: Generates article cover images with 5 dimensions (type, palette, rendering, text, mood) combining 9 color palettes and 6 rendering styles. Supports cinematic (2.35:1), widescreen (16:9), and square (1:1) aspects. Use when user asks to "generate cover image", "create article cover", "make cover", or mentions "封面图".
description: Generates article cover images with 5 dimensions (type, palette, rendering, text, mood) combining 9 color palettes and 6 rendering styles. Supports cinematic (2.35:1), widescreen (16:9), and square (1:1) aspects. Use when user asks to "generate cover image", "create article cover", or "make cover".
---
# Cover Image Generator
@@ -10,29 +10,23 @@ Generate elegant cover images for articles with 5-dimensional customization.
## Usage
```bash
# Auto-select all dimensions based on content
# Auto-select dimensions based on content
/baoyu-cover-image path/to/article.md
# Quick mode: skip confirmation, use auto-selection
# Quick mode: skip confirmation
/baoyu-cover-image article.md --quick
# Specify dimensions (new 5D system)
# Specify dimensions
/baoyu-cover-image article.md --type conceptual --palette warm --rendering flat-vector
/baoyu-cover-image article.md --text title-subtitle --mood bold
# Style presets (backward-compatible shorthand for palette + rendering)
# Style presets (shorthand for palette + rendering)
/baoyu-cover-image article.md --style blueprint
/baoyu-cover-image article.md --style blueprint --rendering hand-drawn # override rendering
# Visual only (no title text)
/baoyu-cover-image article.md --no-title
# With reference images
/baoyu-cover-image article.md --ref style-ref.png
# Direct content input
/baoyu-cover-image
[paste content]
# Direct input with options
/baoyu-cover-image --palette mono --rendering digital --aspect 1:1 --quick
/baoyu-cover-image --palette mono --aspect 1:1 --quick
[paste content]
```
@@ -40,115 +34,70 @@ Generate elegant cover images for articles with 5-dimensional customization.
| Option | Description |
|--------|-------------|
| `--type <name>` | Cover type: hero, conceptual, typography, metaphor, scene, minimal |
| `--palette <name>` | Color palette: warm, elegant, cool, dark, earth, vivid, pastel, mono, retro |
| `--rendering <name>` | Rendering style: flat-vector, hand-drawn, painterly, digital, pixel, chalk |
| `--style <name>` | Preset shorthand (expands to palette + rendering, see [Style Presets](references/style-presets.md)) |
| `--text <level>` | Text density: none, title-only, title-subtitle, text-rich |
| `--mood <level>` | Emotional intensity: subtle, balanced, bold |
| `--type <name>` | hero, conceptual, typography, metaphor, scene, minimal |
| `--palette <name>` | warm, elegant, cool, dark, earth, vivid, pastel, mono, retro |
| `--rendering <name>` | flat-vector, hand-drawn, painterly, digital, pixel, chalk |
| `--style <name>` | Preset shorthand (see [Style Presets](references/style-presets.md)) |
| `--text <level>` | none, title-only, title-subtitle, text-rich |
| `--mood <level>` | subtle, balanced, bold |
| `--font <name>` | clean, handwritten, serif, display |
| `--aspect <ratio>` | 16:9 (default), 2.35:1, 4:3, 3:2, 1:1, 3:4 |
| `--lang <code>` | Title language (en, zh, ja, etc.) |
| `--no-title` | Alias for `--text none` |
| `--quick` | Skip confirmation, use auto-selection for missing dimensions |
| `--quick` | Skip confirmation, use auto-selection |
| `--ref <files...>` | Reference images for style/composition guidance |
## Five Dimensions
| Dimension | Controls | Values | Default |
|-----------|----------|--------|---------|
| **Type** | Visual composition, information structure | hero, conceptual, typography, metaphor, scene, minimal | auto |
| **Palette** | Colors, color scheme, decorative hints | warm, elegant, cool, dark, earth, vivid, pastel, mono, retro | auto |
| **Rendering** | Line quality, texture, depth, element style | flat-vector, hand-drawn, painterly, digital, pixel, chalk | auto |
| **Text** | Text density, information hierarchy | none, title-only, title-subtitle, text-rich | title-only |
| **Mood** | Emotional intensity, visual weight | subtle, balanced, bold | balanced |
| Dimension | Values | Default |
|-----------|--------|---------|
| **Type** | hero, conceptual, typography, metaphor, scene, minimal | auto |
| **Palette** | warm, elegant, cool, dark, earth, vivid, pastel, mono, retro | auto |
| **Rendering** | flat-vector, hand-drawn, painterly, digital, pixel, chalk | auto |
| **Text** | none, title-only, title-subtitle, text-rich | title-only |
| **Mood** | subtle, balanced, bold | balanced |
| **Font** | clean, handwritten, serif, display | clean |
Dimensions can be freely combined. Auto-selection rules: [references/auto-selection.md](references/auto-selection.md)
Auto-selection rules: [references/auto-selection.md](references/auto-selection.md)
## Type Gallery
## Galleries
| Type | Description | Best For |
|------|-------------|----------|
| `hero` | Large visual impact, title overlay | Product launch, brand promotion, major announcements |
| `conceptual` | Concept visualization, abstract core ideas | Technical articles, methodology, architecture design |
| `typography` | Text-focused layout, prominent title | Opinion pieces, quotes, insights |
| `metaphor` | Visual metaphor, concrete expressing abstract | Philosophy, growth, personal development |
| `scene` | Atmospheric scene, narrative feel | Stories, travel, lifestyle |
| `minimal` | Minimalist composition, generous whitespace | Zen, focus, core concepts |
**Types**: hero, conceptual, typography, metaphor, scene, minimal
→ Details: [references/types.md](references/types.md)
Type composition details: [references/types.md](references/types.md)
**Palettes**: warm, elegant, cool, dark, earth, vivid, pastel, mono, retro
→ Details: [references/palettes/](references/palettes/)
## Palette Gallery
**Renderings**: flat-vector, hand-drawn, painterly, digital, pixel, chalk
→ Details: [references/renderings/](references/renderings/)
| Palette | Vibe | Primary Colors |
|---------|------|----------------|
| `warm` | Friendly, approachable | Orange, golden yellow, terracotta |
| `elegant` | Sophisticated, refined | Soft coral, muted teal, dusty rose |
| `cool` | Technical, professional | Engineering blue, navy, cyan |
| `dark` | Cinematic, premium | Electric purple, cyan, magenta |
| `earth` | Natural, organic | Forest green, sage, earth brown |
| `vivid` | Energetic, bold | Bright red, neon green, electric blue |
| `pastel` | Gentle, whimsical | Soft pink, mint, lavender |
| `mono` | Clean, focused | Black, near-black, white |
| `retro` | Nostalgic, vintage | Muted orange, dusty pink, maroon |
**Text Levels**: none (pure visual) | title-only (default) | title-subtitle | text-rich (with tags)
→ Details: [references/dimensions/text.md](references/dimensions/text.md)
Palette definitions: [references/palettes/](references/palettes/)
**Mood Levels**: subtle (low contrast) | balanced (default) | bold (high contrast)
→ Details: [references/dimensions/mood.md](references/dimensions/mood.md)
## Rendering Gallery
| Rendering | Description | Key Characteristics |
|-----------|-------------|---------------------|
| `flat-vector` | Clean modern vector | Uniform outlines, flat fills, geometric icons |
| `hand-drawn` | Sketchy organic illustration | Imperfect strokes, paper texture, doodles |
| `painterly` | Soft watercolor/paint | Brush strokes, color bleeds, soft edges |
| `digital` | Polished modern digital | Precise edges, subtle gradients, UI components |
| `pixel` | Retro 8-bit pixel art | Pixel grid, dithering, chunky shapes |
| `chalk` | Chalk on blackboard | Chalk strokes, dust effects, board texture |
Rendering definitions: [references/renderings/](references/renderings/)
## Text & Mood
| Text Level | Title | Subtitle | Tags | Use Case |
|------------|:-----:|:--------:|:----:|----------|
| `none` | - | - | - | Pure visual, no text |
| `title-only` | ✓ (≤8 字) | - | - | Simple headline (default) |
| `title-subtitle` | ✓ | ✓ (≤15 字) | - | Title + supporting context |
| `text-rich` | ✓ | ✓ | ✓ (2-4) | Information-dense |
| Mood | Contrast | Saturation | Weight | Use Case |
|------|:--------:|:----------:|:------:|----------|
| `subtle` | Low | Muted | Light | Corporate, thought leadership |
| `balanced` | Medium | Normal | Medium | General articles (default) |
| `bold` | High | Vivid | Heavy | Announcements, promotions |
Full guides: [references/dimensions/text.md](references/dimensions/text.md) | [references/dimensions/mood.md](references/dimensions/mood.md)
## Style Presets & Compatibility
- **Style Presets**: `--style X` expands to palette + rendering. See [references/style-presets.md](references/style-presets.md)
- **Compatibility Matrices**: Palette×Rendering, Type×Rendering, Type×Text, Type×Mood. See [references/compatibility.md](references/compatibility.md)
- ✓✓ = highly recommended | ✓ = compatible | ✗ = not recommended
**Fonts**: clean (sans-serif) | handwritten | serif | display (bold decorative)
→ Details: [references/dimensions/font.md](references/dimensions/font.md)
## File Structure
Output directory depends on `default_output_dir` preference:
| Preference | Output Path |
|------------|-------------|
| `same-dir` | `{article-dir}/` |
| `imgs-subdir` | `{article-dir}/imgs/` |
| `independent` (default) | `cover-image/{topic-slug}/` |
| Pasted content | `cover-image/{topic-slug}/` (always) |
Output directory per `default_output_dir` preference:
- `same-dir`: `{article-dir}/`
- `imgs-subdir`: `{article-dir}/imgs/`
- `independent` (default): `cover-image/{topic-slug}/`
```
<output-dir>/
├── source-{slug}.{ext} # Source files (text, images, etc.)
├── source-{slug}.{ext} # Source files
├── refs/ # Reference images (if provided)
│ ├── ref-01-{slug}.{ext}
│ └── ref-01-{slug}.md # Description file
├── prompts/cover.md # Generation prompt
└── cover.png # Output image
```
**Slug**: Extract main topic (2-4 words, kebab-case). Example: "The Future of AI" → `future-of-ai`
**Conflict**: If directory exists, append timestamp: `{topic-slug}-YYYYMMDD-HHMMSS`
**Source Files**: Copy all sources with naming `source-{slug}.{ext}` (multiple supported)
**Slug**: 2-4 words, kebab-case. Conflict: append `-YYYYMMDD-HHMMSS`
## Workflow
@@ -156,9 +105,9 @@ Output directory depends on `default_output_dir` preference:
```
Cover Image Progress:
- [ ] Step 0: Check preferences (EXTEND.md) ⚠️ REQUIRED if not found
- [ ] Step 1: Analyze content + determine output directory ⚠️ MUST ask if not configured
- [ ] Step 2: Confirm options (5 dimensions) ⚠️ REQUIRED unless --quick or all specified
- [ ] Step 0: Check preferences (EXTEND.md) ⛔ BLOCKING
- [ ] Step 1: Analyze content + save refs + determine output dir
- [ ] Step 2: Confirm options (6 dimensions) ⚠️ unless --quick
- [ ] Step 3: Create prompt
- [ ] Step 4: Generate image
- [ ] Step 5: Completion report
@@ -167,73 +116,74 @@ Cover Image Progress:
### Flow
```
Input → [Step 0: Preferences/Setup] → Analyze → [Output Dir ⚠️][Confirm: 5 Dimensions] → Prompt → Generate → Complete
(skip if --quick or all specified)
Input → [Step 0: Preferences] ─┬─ Found → Continue
└─ Not found → First-Time Setup ⛔ BLOCKING → Save EXTEND.md → Continue
Analyze + Save Refs → [Output Dir] → [Confirm: 6 Dimensions] → Prompt → Generate → Complete
(skip if --quick or all specified)
```
### Step 0: Load Preferences (EXTEND.md) ⚠️
**Purpose**: Load user preferences or run first-time setup. **Do NOT skip setup if EXTEND.md not found.**
Use Bash to check EXTEND.md existence (priority order):
### Step 0: Load Preferences ⛔ BLOCKING
Check EXTEND.md existence (priority: project → user):
```bash
# Check project-level first
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-cover-image/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-cover-image/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-cover-image/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-cover-image/EXTEND.md") { "user" }
```
| Result | Action |
|--------|--------|
| Found | Read, parse, display preferences summary → Continue to Step 1 |
| Not found | ⚠️ MUST run first-time setup ([references/config/first-time-setup.md](references/config/first-time-setup.md)) → Then continue to Step 1 |
| Found | Load, display summary → Continue |
| Not found | ⛔ Run first-time setup ([references/config/first-time-setup.md](references/config/first-time-setup.md)) → Save → Continue |
**Preferences Summary** (when found):
```
Preferences loaded from [project/user]:
• Watermark: [enabled/disabled] [content if enabled]
• Type/Palette/Rendering: [value or "auto"]
• Text: [value or "title-only"] | Mood: [value or "balanced"]
• Aspect: [default_aspect] | Output: [dir or "not set — will ask in Step 1.5"]
• Quick mode: [enabled/disabled] | Language: [value or "auto"]
```
**EXTEND.md Supports**: Watermark | Preferred type | Preferred palette | Preferred rendering | Preferred text | Preferred mood | Default aspect ratio | Default output directory | Quick mode | Custom palette definitions | Language preference
Schema: [references/config/preferences-schema.md](references/config/preferences-schema.md)
**CRITICAL**: If not found, complete setup BEFORE any other steps or questions.
### Step 1: Analyze Content
1. **Save source content** (if pasted, save to `source.md` in target directory; if file path, use as-is)
2. **Content analysis**: Extract topic, core message, tone, keywords; identify visual metaphors; detect content type
3. **Language detection**: Detect source language, note user's input language, compare with EXTEND.md preference
4. **Determine output directory** per File Structure rules. If no `default_output_dir` preference + file path input, include in Step 2 Q4
1. **Save reference images** (if provided) → [references/workflow/reference-images.md](references/workflow/reference-images.md)
2. **Save source content** (if pasted, save to `source.md`)
3. **Analyze content**: topic, tone, keywords, visual metaphors
4. **Deep analyze references** ⚠️: Extract specific, concrete elements (see reference-images.md)
5. **Detect language**: Compare source, user input, EXTEND.md preference
6. **Determine output directory**: Per File Structure rules
### Step 2: Confirm Options ⚠️
Validate all 5 dimensions + aspect ratio. Full confirmation flow: [references/workflow/confirm-options.md](references/workflow/confirm-options.md)
**Skip Conditions**:
Full confirmation flow: [references/workflow/confirm-options.md](references/workflow/confirm-options.md)
| Condition | Skipped | Still Asked |
|-----------|---------|-------------|
| `--quick` or `quick_mode: true` | 5 dimensions | Aspect ratio (unless `--aspect`) |
| All 5 + `--aspect` specified | All | None |
| `--quick` or `quick_mode: true` | 6 dimensions | Aspect ratio (unless `--aspect`) |
| All 6 + `--aspect` specified | All | None |
### Step 3: Create Prompt
Save to `prompts/cover.md`. Full template: [references/workflow/prompt-template.md](references/workflow/prompt-template.md)
Save to `prompts/cover.md`. Template: [references/workflow/prompt-template.md](references/workflow/prompt-template.md)
**CRITICAL - References in Frontmatter**:
- Files saved to `refs/` → Add to frontmatter `references` list
- Style extracted verbally (no file) → Omit `references`, describe in body
- Before writing → Verify: `test -f refs/ref-NN-{slug}.{ext}`
**Reference elements in body** MUST be detailed, prefixed with "MUST"/"REQUIRED", with integration approach.
### Step 4: Generate Image
1. Backup existing `cover.png` `cover-backup-YYYYMMDD-HHMMSS.png` (if regenerating)
2. Check available image generation skills; if multiple, ask user preference
3. Call selected skill with prompt file path, output path (`cover.png`), aspect ratio
4. On failure: auto-retry once before reporting error
1. **Backup existing** `cover.png` if regenerating
2. **Check image generation skills**; if multiple, ask preference
3. **Process references** from prompt frontmatter:
- `direct` usage → pass via `--ref` (use ref-capable backend)
- `style`/`palette` → extract traits, append to prompt
4. **Generate**: Call skill with prompt file, output path, aspect ratio
5. On failure: auto-retry once
### Step 5: Completion Report
@@ -242,49 +192,49 @@ Cover Generated!
Topic: [topic]
Type: [type] | Palette: [palette] | Rendering: [rendering]
Text: [text] | Mood: [mood] | Aspect: [ratio]
Title: [title text or "visual only"]
Text: [text] | Mood: [mood] | Font: [font] | Aspect: [ratio]
Title: [title or "visual only"]
Language: [lang] | Watermark: [enabled/disabled]
References: [N images or "extracted style" or "none"]
Location: [directory path]
Files:
✓ source-{slug}.{ext}
✓ prompts/cover.md
✓ cover.png
[✓ cover-backup-{timestamp}.png (if regenerated)]
```
## Image Modification
| Action | Steps |
|--------|-------|
| **Regenerate** | Backup existing → **Update prompt file FIRST** → Regenerate with same settings |
| **Change dimension** | Backup existing → Confirm new value → **Update prompt file FIRST** → Regenerate |
| **Regenerate** | Backup Update prompt file FIRST → Regenerate |
| **Change dimension** | Backup → Confirm new value → Update prompt → Regenerate |
**IMPORTANT**: When regenerating, ALWAYS update the prompt file (`prompts/cover.md`) FIRST before regenerating. This ensures changes are documented and reproducible.
## Composition Principles
All modifications automatically backup existing `cover.png` before regenerating.
- **Whitespace**: 40-60% breathing room
- **Visual anchor**: Main element centered or offset left
- **Characters**: Simplified silhouettes; NO realistic humans
- **Title**: Use exact title from user/source; never invent
## Notes
## Extension Support
- Cover must be readable at small preview sizes
- Visual metaphors > literal representations
- Title: max 8 chars, readable, impactful
- Two confirmation points: Step 0 (first-time setup) + Step 2 (options) - skip Step 2 with `--quick`
- Use confirmed language for title text
- Maintain watermark consistency if enabled
- Check compatibility matrices when selecting combinations
- `--no-title` is alias for `--text none`
- `--style` presets are backward-compatible; explicit `--palette`/`--rendering` override preset values
Custom configurations via EXTEND.md. See **Step 0** for paths.
Supports: Watermark | Preferred dimensions | Default aspect/output | Quick mode | Custom palettes | Language
Schema: [references/config/preferences-schema.md](references/config/preferences-schema.md)
## References
**Dimensions**: [text.md](references/dimensions/text.md) | [mood.md](references/dimensions/mood.md)
**Dimensions**: [text.md](references/dimensions/text.md) | [mood.md](references/dimensions/mood.md) | [font.md](references/dimensions/font.md)
**Palettes**: [references/palettes/](references/palettes/)
**Renderings**: [references/renderings/](references/renderings/)
**Types**: [references/types.md](references/types.md)
**Auto-Selection**: [references/auto-selection.md](references/auto-selection.md)
**Style Presets**: [references/style-presets.md](references/style-presets.md)
**Compatibility**: [references/compatibility.md](references/compatibility.md)
**Types**: [references/types.md](references/types.md)
**Workflow**: [confirm-options.md](references/workflow/confirm-options.md) | [prompt-template.md](references/workflow/prompt-template.md)
**Visual Elements**: [references/visual-elements.md](references/visual-elements.md)
**Workflow**: [confirm-options.md](references/workflow/confirm-options.md) | [prompt-template.md](references/workflow/prompt-template.md) | [reference-images.md](references/workflow/reference-images.md)
**Config**: [preferences-schema.md](references/config/preferences-schema.md) | [first-time-setup.md](references/config/first-time-setup.md) | [watermark-guide.md](references/config/watermark-guide.md)
@@ -58,3 +58,14 @@ Default: `title-only`
| Launch, announcement, promotion, event, gaming, entertainment | `bold` |
Default: `balanced`
## Auto Font Selection
| Signals | Font |
|---------|------|
| Personal, lifestyle, human, warm, friendly, story | `handwritten` |
| Technical, professional, clean, modern, minimal, data | `clean` |
| Editorial, academic, luxury, classic, literary | `serif` |
| Announcement, entertainment, promotion, bold, event, gaming | `display` |
Default: `clean`
@@ -39,8 +39,8 @@ Apply the specified rendering's characteristics:
### Text (Density Level)
- `none`: No text elements, full visual area
- `title-only`: Single headline (≤8 characters), 85% visual area
- `title-subtitle`: Title + context (≤15 chars), 75% visual area
- `title-only`: Single headline, 85% visual area
- `title-subtitle`: Title + context, 75% visual area
- `text-rich`: Title + subtitle + 2-4 keyword tags, 60% visual area
### Mood (Emotional Intensity)
@@ -50,17 +50,44 @@ Apply the specified rendering's characteristics:
## Text Style (When Title Included)
- Title text: Large, eye-catching, max 8 characters
- Subtitle: Secondary, max 15 characters (if title-subtitle or text-rich)
- **Title source**: Use the exact title provided by user, or extract from source content. Do NOT invent or modify titles.
- Title text: Large, eye-catching, faithful to source
- Subtitle: Secondary element (if title-subtitle or text-rich)
- Tags: 2-4 keyword badges (if text-rich)
- Font style harmonizes with rendering style
- **Engagement hooks**: Use numbers ("3个关键"), questions ("Why?"), contrasts ("A vs B"), pain points ("别再X了"), or suspense ("隐藏的X") for compelling titles
## Composition Guidance
**Icon Vocabulary**: Represent concepts with simple, recognizable icons rather than detailed illustrations. Use the rendering style to determine icon complexity (flat-vector = geometric, hand-drawn = sketchy, etc.)
### Layout Principles
**Character Handling**: When people are needed, use simplified silhouettes or abstract representations. NO realistic faces, detailed anatomy, or photographic representations.
- **Generous whitespace**: Maintain 40-60% breathing room; avoid cluttered compositions
- **Visual anchor placement**: Main element centered or offset left (reserve right side for title if included)
- **Information hierarchy**: One dominant focal point, 1-2 supporting elements, decorative accents
- **Clean backgrounds**: Solid colors or subtle gradients; no complex textures or patterns
### Icon & Symbol Vocabulary
Represent concepts with simple, recognizable icons rather than detailed illustrations:
| Category | Examples |
|----------|----------|
| Tech | Code window, gear, circuit, cloud, lock, API brackets |
| Ideas | Lightbulb, rocket, target, puzzle, key, magnifier |
| Communication | Speech bubble, chat dots, megaphone, mail |
| Growth | Plant/sprout, tree, arrow, chart, mountain |
| Tools | Wrench, pencil, brush, checklist, clock |
Use the rendering style to determine icon complexity (flat-vector = geometric, hand-drawn = sketchy, etc.)
Full library: [references/visual-elements.md](visual-elements.md)
### Character Handling
When people are needed:
- Use simplified silhouettes or abstract stick figures
- Symbolic representations (head + shoulders outline)
- NO realistic faces, detailed anatomy, or photographic representations
- Cartoon/icon style consistent with rendering choice
## Mood Application
@@ -77,6 +104,16 @@ Apply mood adjustments to the base palette:
- Use the same language as the content provided below for any text elements
- Match punctuation style to the content language
## Reference Images
When reference images are provided:
- **Style extraction**: Identify rendering technique, line quality, texture, and visual vocabulary
- **Composition learning**: Note layout patterns, whitespace usage, element placement
- **Mood matching**: Capture the emotional tone and visual weight
- **Adaptation**: Apply extracted characteristics while respecting the specified Type, Palette, and Rendering dimensions
- **Priority**: If reference style conflicts with specified dimensions, dimensions take precedence for structural choices; reference influences decorative details
---
Please generate the cover image based on the content provided below:
@@ -48,3 +48,12 @@
| metaphor | ✓✓ | ✓✓ | ✓ |
| scene | ✓✓ | ✓✓ | ✓ |
| minimal | ✓✓ | ✓✓ | ✗ |
## Font × Rendering
| | flat-vector | hand-drawn | painterly | digital | pixel | chalk |
|---|:---:|:---:|:---:|:---:|:---:|:---:|
| clean | ✓✓ | ✗ | ✗ | ✓✓ | ✓ | ✗ |
| handwritten | ✓ | ✓✓ | ✓✓ | ✓ | ✗ | ✓✓ |
| serif | ✓ | ✗ | ✓ | ✓✓ | ✗ | ✗ |
| display | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
@@ -9,6 +9,14 @@ description: First-time setup flow for baoyu-cover-image preferences
When no EXTEND.md is found, guide user through preference setup.
**⛔ BLOCKING OPERATION**: This setup MUST complete before ANY other workflow steps. Do NOT:
- Ask about reference images
- Ask about content/article
- Ask about dimensions (type, palette, rendering)
- Proceed to content analysis
ONLY ask the questions in this setup flow, save EXTEND.md, then continue.
## Setup Flow
```
@@ -103,8 +103,8 @@ custom_palettes:
| Value | Description |
|-------|-------------|
| `none` | Pure visual, no text elements |
| `title-only` | Single headline (≤8 characters) |
| `title-subtitle` | Title + subtitle (≤15 characters) |
| `title-only` | Single headline |
| `title-subtitle` | Title + subtitle |
| `text-rich` | Title + subtitle + keyword tags (2-4) |
## Mood Options
@@ -0,0 +1,163 @@
---
name: font-dimension
description: Typography style dimension for cover images
---
# Font Dimension
Controls typography style and character feel.
## Values
| Font | Visual Style | Line Quality | Character |
|------|--------------|--------------|-----------|
| `clean` | Geometric sans-serif | Sharp, uniform | Modern, precise, neutral |
| `handwritten` | Hand-lettered, brush | Organic, varied | Warm, personal, friendly |
| `serif` | Classic serifs, elegant | Refined, structured | Editorial, authoritative |
| `display` | Bold, decorative | Heavy, expressive | Attention-grabbing, playful |
## Detail
### clean
Modern, universal typography with neutral character.
**Characteristics**:
- Geometric sans-serif letterforms
- Sharp, uniform line weight
- Clean edges, no flourishes
- High readability at all sizes
- Minimal personality, maximum clarity
**Use Cases**:
- Technical documentation
- Professional/corporate content
- Minimal design approaches
- Data-driven articles
- Modern brand aesthetics
**Prompt Hints**:
- Use clean geometric sans-serif typography
- Modern, minimal letterforms
- Sharp edges, uniform stroke weight
- High contrast against background
### handwritten
Warm, organic typography with personal character.
**Characteristics**:
- Hand-lettered or brush style
- Organic, varied line weight
- Natural imperfections
- Approachable, human feel
- Casual yet intentional
**Use Cases**:
- Personal stories
- Lifestyle content
- Wellness and self-improvement
- Creative tutorials
- Friendly brand voices
**Prompt Hints**:
- Use warm hand-lettered typography with organic brush strokes
- Friendly, personal feel
- Natural variation in stroke weight
- Approachable, human character
### serif
Classic, elegant typography with editorial authority.
**Characteristics**:
- Traditional serif letterforms
- Refined, structured strokes
- Elegant proportions
- Timeless sophistication
- Formal, trustworthy feel
**Use Cases**:
- Editorial content
- Academic articles
- Luxury brand content
- Historical topics
- Literary pieces
**Prompt Hints**:
- Use elegant serif typography with refined letterforms
- Classic, editorial character
- Structured, proportional spacing
- Authoritative, sophisticated feel
### display
Bold, decorative typography for maximum impact.
**Characteristics**:
- Heavy, expressive letterforms
- Decorative elements
- Strong visual presence
- Playful or dramatic character
- Designed for headlines
**Use Cases**:
- Announcements
- Entertainment content
- Promotional materials
- Event marketing
- Gaming topics
**Prompt Hints**:
- Use bold decorative display typography
- Heavy, expressive headlines
- Strong visual impact
- Attention-grabbing character
## Default
`clean` — Universal, pairs well with most rendering styles.
## Rendering Compatibility
| Font × Rendering | flat-vector | hand-drawn | painterly | digital | pixel | chalk |
|------------------|:-----------:|:----------:|:---------:|:-------:|:-----:|:-----:|
| clean | ✓✓ | ✗ | ✗ | ✓✓ | ✓ | ✗ |
| handwritten | ✓ | ✓✓ | ✓✓ | ✓ | ✗ | ✓✓ |
| serif | ✓ | ✗ | ✓ | ✓✓ | ✗ | ✗ |
| display | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
✓✓ = highly recommended | ✓ = compatible | ✗ = not recommended
## Type Compatibility
| Font × Type | hero | conceptual | typography | metaphor | scene | minimal |
|-------------|:----:|:----------:|:----------:|:--------:|:-----:|:-------:|
| clean | ✓ | ✓✓ | ✓✓ | ✓ | ✗ | ✓✓ |
| handwritten | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
| serif | ✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ |
| display | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✗ |
## Palette Interaction
Font style adapts to palette characteristics:
| Palette Category | clean | handwritten | serif | display |
|------------------|-------|-------------|-------|---------|
| Warm (warm, earth, pastel) | Softer weight | Natural fit | Warm tones | Playful energy |
| Cool (cool, mono, elegant) | Perfect match | Contrast | Classic pairing | Bold statement |
| Dark (dark, vivid) | High contrast | Glow effects | Dramatic | Maximum impact |
| Vintage (retro) | Modern contrast | Nostalgic fit | Period-appropriate | Retro headlines |
## Auto Selection
When `--font` is omitted, select based on signals:
| Signals | Font |
|---------|------|
| Personal, lifestyle, human, warm, friendly, story | `handwritten` |
| Technical, professional, clean, modern, minimal, data | `clean` |
| Editorial, academic, luxury, classic, literary | `serif` |
| Announcement, entertainment, promotion, bold, event, gaming | `display` |
Default: `clean`
@@ -12,8 +12,8 @@ Controls text density and information hierarchy on cover images.
| Value | Title | Subtitle | Tags | Visual Area |
|-------|:-----:|:--------:|:----:|:-----------:|
| `none` | - | - | - | 100% |
| `title-only` | ✓ (≤8字) | - | - | 85% |
| `title-subtitle` | ✓ | ✓ (≤15字) | - | 75% |
| `title-only` | ✓ | - | - | 85% |
| `title-subtitle` | ✓ | ✓ | - | 75% |
| `text-rich` | ✓ | ✓ | ✓ (2-4) | 60% |
## Detail
@@ -43,24 +43,14 @@ Single headline, maximum impact.
- Strong brand recognition
**Composition**:
- Title: ≤8 characters, prominent
- Title: prominent placement
- Reserved zone: top or bottom 15%
- Visual supports title message
**Title Guidelines**:
- Punchy, action-oriented
- Use exact title from source content or user-provided title
- Do NOT invent or modify titles
- Match content language
- Use engagement hooks (see below)
**Engagement Hooks**:
| Hook Type | Examples (EN) | Examples (ZH) |
|-----------|---------------|---------------|
| Numbers | "3 Traps", "5 Keys" | "3个陷阱", "5个关键" |
| Questions | "Why X?", "How?" | "为什么X?", "怎么做?" |
| Contrasts | "A vs B", "Old→New" | "A还是B", "旧→新" |
| Pain Points | "Stop X", "Never Do" | "别再X了", "千万别" |
| Suspense | "Hidden X", "Secret" | "隐藏的X", "秘密" |
### title-subtitle
@@ -72,14 +62,14 @@ Title with supporting context.
- Content with dual messages
**Composition**:
- Title: ≤8 characters, primary
- Subtitle: ≤15 characters, secondary
- Title: primary element
- Subtitle: secondary element
- Reserved zone: 25%
- Clear hierarchy between title/subtitle
**Title Guidelines**:
- Use engagement hooks: numbers, questions, contrasts, pain points, suspense
- Punchy, action-oriented
- Use exact title from source content or user-provided title
- Do NOT invent or modify titles
**Subtitle Guidelines**:
- Clarify or contextualize title
@@ -104,8 +94,8 @@ Information-dense cover with multiple text elements.
- Clear visual hierarchy
**Title Guidelines**:
- Use engagement hooks: numbers, questions, contrasts, pain points, suspense
- Punchy, action-oriented
- Use exact title from source content or user-provided title
- Do NOT invent or modify titles
**Tag Guidelines**:
- 2-4 tags maximum
@@ -22,6 +22,7 @@
| `vector-illustration` | `retro` | `flat-vector` |
| `vintage` | `retro` | `hand-drawn` |
| `warm` | `warm` | `hand-drawn` |
| `warm-flat` | `warm` | `flat-vector` |
| `watercolor` | `earth` | `painterly` |
## Override Examples
@@ -0,0 +1,101 @@
# Visual Elements Library
Icon and symbol vocabulary organized by topic. Use these as building blocks for cover compositions.
## Tech & Development
| Element | Use For |
|---------|---------|
| Code window / Terminal | Programming, development |
| Gear / Cog | Engineering, settings, process |
| Circuit board / Chip | Hardware, AI, computing |
| Binary / Data stream | Data, algorithms |
| API brackets `</>` | Web development, APIs |
| Cloud | Cloud computing, SaaS |
| Lock / Shield | Security, privacy |
| Network nodes | Distributed systems, connections |
## Ideas & Innovation
| Element | Use For |
|---------|---------|
| Lightbulb | Ideas, insights, innovation |
| Rocket | Launch, growth, startups |
| Target / Bullseye | Goals, precision, focus |
| Puzzle piece | Problem solving, integration |
| Key | Solutions, access, unlocking |
| Magnifying glass | Analysis, search, discovery |
| Chart / Graph | Data, trends, growth |
| Arrow / Path | Direction, journey, progress |
## Communication & Collaboration
| Element | Use For |
|---------|---------|
| Speech bubble | Communication, dialogue |
| Chat dots `...` | Conversation, messaging |
| Handshake | Partnership, agreement |
| Team / Figures | Collaboration, community |
| Mail / Envelope | Notifications, outreach |
| Megaphone | Announcements, marketing |
| Network / Web | Social, connections |
## Nature & Growth
| Element | Use For |
|---------|---------|
| Plant / Sprout | Growth, organic, sustainability |
| Tree | Established, structure, branching |
| Leaf | Eco, natural, fresh |
| Sun / Rays | Energy, positivity, new beginnings |
| Mountain | Challenge, achievement, scale |
| Wave | Flow, change, rhythm |
| Seed → Plant | Transformation, potential |
## Tools & Actions
| Element | Use For |
|---------|---------|
| Wrench / Hammer | Building, fixing, tools |
| Pencil / Pen | Writing, creation, editing |
| Brush | Design, creativity, art |
| Scissors | Cutting, editing, trimming |
| Clock / Timer | Time, scheduling, deadlines |
| Calendar | Planning, events, milestones |
| Checklist / Checkbox | Tasks, completion, validation |
## Abstract Concepts
| Element | Use For |
|---------|---------|
| Infinity ∞ | Continuous, endless, loops |
| Yin-yang | Balance, duality, harmony |
| Spiral | Evolution, recursion, cycles |
| Stack / Layers | Depth, hierarchy, structure |
| Bridge | Connection, transition, spanning |
| Door / Portal | Opportunity, entry, access |
| Mirror / Reflection | Self-improvement, analysis |
## Combination Patterns
Create visual metaphors by combining elements:
| Combination | Represents |
|-------------|------------|
| Lightbulb + Gear | Innovative engineering |
| Plant + Code | Organic tech growth |
| Rocket + Target | Precise acceleration |
| Key + Lock | Security solutions |
| Bridge + People | Team connections |
| Magnifier + Data | Analytics, insights |
## Rendering-Specific Treatment
| Rendering | Element Style |
|-----------|---------------|
| `flat-vector` | Geometric, simple shapes, uniform fills |
| `hand-drawn` | Sketchy, organic, doodle-like |
| `painterly` | Soft edges, brush strokes |
| `digital` | Precise, gradient hints, polished |
| `pixel` | 8-bit chunky, grid-aligned |
| `chalk` | Dusty, textured, board style |
@@ -2,22 +2,22 @@
## Purpose
Validate all 5 dimensions + aspect ratio.
Validate all 6 dimensions + aspect ratio.
## Skip Conditions
| Condition | Skipped Questions | Still Asked |
|-----------|-------------------|-------------|
| `--quick` flag | Type, Palette, Rendering, Text, Mood | **Aspect Ratio** (unless `--aspect` specified) |
| All 5 dimensions + `--aspect` specified | All | None |
| `quick_mode: true` in EXTEND.md | Type, Palette, Rendering, Text, Mood | **Aspect Ratio** (unless `--aspect` specified) |
| Otherwise | None | All 6 questions |
| `--quick` flag | Type, Palette, Rendering, Text, Mood, Font | **Aspect Ratio** (unless `--aspect` specified) |
| All 6 dimensions + `--aspect` specified | All | None |
| `quick_mode: true` in EXTEND.md | Type, Palette, Rendering, Text, Mood, Font | **Aspect Ratio** (unless `--aspect` specified) |
| Otherwise | None | All 7 questions |
**Important**: Aspect ratio is ALWAYS asked unless explicitly specified via `--aspect` CLI flag. User presets in EXTEND.md are shown as recommended option, not auto-selected.
## Quick Mode Output
When skipping 5 dimensions:
When skipping 6 dimensions:
```
Quick Mode: Auto-selected dimensions
@@ -26,8 +26,9 @@ Quick Mode: Auto-selected dimensions
• Rendering: [rendering] ([reason])
• Text: [text] ([reason])
• Mood: [mood] ([reason])
• Font: [font] ([reason])
[Then ask Question 6: Aspect Ratio]
[Then ask Question 7: Aspect Ratio]
```
## Confirmation Flow
@@ -91,7 +92,26 @@ options:
description: "Polished, precise, subtle gradients"
```
### Q4: Other Settings (skip if all remaining dimensions already specified)
### Q4: Font (skip if `--font`)
```yaml
header: "Font"
question: "Which font style?"
multiSelect: false
options:
- label: "[auto-recommended font] (Recommended)"
description: "[reason based on content signals]"
- label: "clean"
description: "Modern geometric sans-serif - tech, professional"
- label: "handwritten"
description: "Warm hand-lettered - personal, friendly"
- label: "serif"
description: "Classic elegant - editorial, luxury"
- label: "display"
description: "Bold decorative - announcements, entertainment"
```
### Q5: Other Settings (skip if all remaining dimensions already specified)
Combine remaining settings into one question. Include: Output Dir (if no preference + file path input), Text, Mood, Aspect. Show auto-selected values as recommended option. User can accept all or type adjustments via "Other".
@@ -3,6 +3,19 @@
Save to `prompts/cover.md`:
```markdown
---
type: cover
palette: [confirmed palette]
rendering: [confirmed rendering]
references:
- ref_id: 01
filename: refs/ref-01-{slug}.{ext}
usage: direct | style | palette
- ref_id: 02
filename: refs/ref-02-{slug}.{ext}
usage: direct | style | palette
---
# Content Context
Article title: [full original title from source]
Content summary: [2-3 sentence summary of key points and themes]
@@ -13,6 +26,7 @@ Cover theme: [2-3 words visual interpretation]
Type: [confirmed type]
Palette: [confirmed palette]
Rendering: [confirmed rendering]
Font: [confirmed font]
Text level: [confirmed text level]
Mood: [confirmed mood]
Aspect ratio: [confirmed ratio]
@@ -21,9 +35,9 @@ Language: [confirmed language]
# Text Elements
[Based on text level:]
- none: "No text elements"
- title-only: "Title: [max 8 chars headline]"
- title-subtitle: "Title: [headline] / Subtitle: [max 15 chars context]"
- text-rich: "Title: [headline] / Subtitle: [context] / Tags: [2-4 keywords]"
- title-only: "Title: [exact title from source or user]"
- title-subtitle: "Title: [title] / Subtitle: [context]"
- text-rich: "Title: [title] / Subtitle: [context] / Tags: [2-4 keywords]"
# Mood Application
[Based on mood level:]
@@ -31,6 +45,13 @@ Language: [confirmed language]
- balanced: "Use medium contrast, normal saturation, balanced visual weight"
- bold: "Use high contrast, vivid saturated colors, heavy visual weight, dynamic energy"
# Font Application
[Based on font style:]
- clean: "Use clean geometric sans-serif typography. Modern, minimal letterforms."
- handwritten: "Use warm hand-lettered typography with organic brush strokes. Friendly, personal feel."
- serif: "Use elegant serif typography with refined letterforms. Classic, editorial character."
- display: "Use bold decorative display typography. Heavy, expressive headlines."
# Composition
Type composition:
- [Type-specific layout and structure]
@@ -46,14 +67,37 @@ Type notes: [key characteristics from type definition]
Palette notes: [key characteristics from palette definition]
[Watermark section if enabled]
[Reference images section if provided — REQUIRED, see below]
```
## Reference-Driven Design ⚠️ HIGH PRIORITY
When reference images are provided, they are the **primary visual input** and MUST strongly influence the output. The cover should look like it belongs to the same visual family as the references.
**Passing `--ref` alone is NOT enough.** Image generation models often ignore reference images unless the prompt text explicitly describes what to reproduce. Always combine `--ref` with detailed textual instructions.
## Content-Driven Design
- Article title and summary inform the visual metaphor choice
- Keywords guide decorative elements and symbols
- The skill controls visual style; the content drives meaning
## Visual Element Selection
Match content themes to icon vocabulary:
| Content Theme | Suggested Elements |
|---------------|-------------------|
| Programming/Dev | Code window, terminal, API brackets, gear |
| AI/ML | Brain, neural network, robot, circuit |
| Growth/Business | Chart, rocket, plant, mountain, arrow |
| Security | Lock, shield, key, fingerprint |
| Communication | Speech bubble, megaphone, mail, handshake |
| Tools/Methods | Wrench, checklist, pencil, puzzle |
Full library: [../visual-elements.md](../visual-elements.md)
## Type-Specific Composition
| Type | Composition Guidelines |
@@ -68,8 +112,8 @@ Palette notes: [key characteristics from palette definition]
## Title Guidelines
When text level includes title:
- Max 8 characters, punchy headline
- Use engagement hooks: numbers ("3个陷阱"), questions ("Why X?"), contrasts ("A vs B"), pain points ("别再X了")
- **Source**: Use the exact title provided by user, or extract from source content
- **Do NOT invent titles**: Stay faithful to the original
- Match confirmed language
## Watermark Application
@@ -82,3 +126,122 @@ The watermark should be legible but not distracting from the main content.
```
Reference: `config/watermark-guide.md`
## Reference Image Handling
When user provides reference images (`--ref` or pasted images):
### ⚠️ CRITICAL - Frontmatter References
**MUST add `references` field in YAML frontmatter** when reference files are saved to `refs/`:
```yaml
---
type: cover
palette: warm
rendering: flat-vector
references:
- ref_id: 01
filename: refs/ref-01-podcast-thumbnail.jpg
usage: style
---
```
| Field | Description |
|-------|-------------|
| `ref_id` | Sequential number (01, 02, ...) |
| `filename` | Relative path from prompt file's parent directory |
| `usage` | `direct` / `style` / `palette` |
**Omit `references` field entirely** if no reference files saved (style extracted verbally only).
### When to Include References in Frontmatter
| Situation | Frontmatter Action | Generation Action |
|-----------|-------------------|-------------------|
| Reference file saved to `refs/` | Add to `references` list ✓ | Pass via `--ref` parameter |
| Style extracted verbally (no file) | Omit `references` field | Describe in prompt body only |
| File path in frontmatter but doesn't exist | ERROR - fix or remove | Generation will fail |
**Before writing prompt with references, verify**: `test -f refs/ref-NN-{slug}.{ext}`
### Reference Usage Types
| Usage | When to Use | Generation Action |
|-------|-------------|-------------------|
| `direct` | Reference matches desired output closely | Pass to `--ref` parameter |
| `style` | Extract visual style characteristics only | Describe style in prompt text |
| `palette` | Extract color palette only | Include colors in prompt |
### Step 1: Analyze References
For each reference image, extract:
- **Style**: Rendering technique, line quality, texture
- **Composition**: Layout, visual hierarchy, focal points
- **Color mood**: Palette characteristics (without specific colors)
- **Elements**: Key visual elements and symbols used
### Step 2: Embed in Prompt ⚠️ CRITICAL
**Passing `--ref` alone is NOT enough.** Image generation models frequently ignore reference images unless the prompt text explicitly and forcefully describes what to reproduce. You MUST always write detailed textual instructions regardless of whether `--ref` is used.
**If file saved (with or without `--ref` support)**:
- Pass ref images via `--ref` parameter if skill supports it
- **ALWAYS** add a detailed mandatory section in the prompt body:
```
# Reference Style — MUST INCORPORATE
CRITICAL: The generated cover MUST visually reference the provided images. The cover must feel like it belongs to the same visual family.
## From Ref 1 ([filename]) — REQUIRED elements:
- [Brand element]: [Specific description of logo/wordmark treatment, e.g., "The logo uses vertical parallel lines (|||) for the letter 'm'. Reproduce this exact treatment."]
- [Signature pattern]: [Specific description, e.g., "Woven intersecting curves forming a diamond/lozenge grid pattern. This MUST appear prominently as a banner, border, or background section."]
- [Colors]: [Exact hex values, e.g., "Dark teal #2D4A3E background, cream #F5F0E0 text"]
- [Typography]: [Specific treatment, e.g., "Uppercase text with wide letter-spacing"]
- [Layout element]: [Specific spatial element, e.g., "Bottom banner strip in dark color"]
## From Ref 2 ([filename]) — REQUIRED elements:
[Same detailed breakdown]
## Integration approach:
[Specific layout instruction describing how reference elements combine with the cover content, e.g., "Use a SPLIT LAYOUT: main illustration area (warm cream background) occupies ~65% of the image, while a dark teal BANNER STRIP (with the woven line pattern from Ref 2) runs along the bottom ~35%, containing branding elements from Ref 1."]
```
**Key rules**:
- Each visual element gets its own bullet with "MUST" or "REQUIRED"
- Descriptions must be **specific enough to reproduce** — not vague ("clean style")
- The integration approach must describe **exact spatial arrangement**
- After generation, verify reference elements are visible; if not, strengthen and regenerate
**If style/palette extracted verbally (NO file saved)**:
- DO NOT add references metadata to prompt
- Append extracted info directly to prompt body using the same MUST INCORPORATE format above:
```
# Reference Style — MUST INCORPORATE (extracted from visual analysis)
CRITICAL: Apply these specific visual elements extracted from the reference images.
## REQUIRED elements:
- [Same detailed bullet format as above]
## Integration approach:
[Same spatial layout instruction]
```
### Reference Analysis Template
Use this format when analyzing reference images. Extract **specific, concrete, reproducible** details — not vague summaries.
| Aspect | Analysis Points | Good Example | Bad Example |
|--------|-----------------|--------------|-------------|
| **Brand elements** | Logos, wordmarks, distinctive typography | "Logo 'm' formed by 3 vertical lines" | "Has a logo" |
| **Signature patterns** | Unique motifs, textures, geometric patterns | "Woven curves forming diamond grid" | "Has patterns" |
| **Colors** | Exact hex values or close approximations | "#2D4A3E dark teal, #F5F0E0 cream" | "Dark and light" |
| **Layout** | Spatial zones, banner placement, proportions | "Bottom 30% is dark banner with branding" | "Has a banner" |
| **Typography** | Font style, weight, case, spacing, position | "Uppercase, wide letter-spacing, right-aligned" | "Has text" |
| **Rendering** | Line quality, texture, depth treatment | "Topographic contour lines as background texture" | "Clean style" |
| **Elements** | Icon vocabulary, decorative motifs | "Geometric intersecting line ornaments at corners" | "Has decorations" |
**Output**: Each extracted element should be written as a **copy-pasteable prompt instruction** prefixed with "MUST" or "REQUIRED".
@@ -0,0 +1,86 @@
# Reference Image Handling
Guide for processing user-provided reference images in cover generation.
## Input Detection
| Input Type | Action |
|------------|--------|
| Image file path provided | Copy to `refs/` → can use `--ref` |
| Image in conversation (no path) | **ASK user for file path** with AskUserQuestion |
| User can't provide path | Extract style/palette verbally → append to prompt (NO frontmatter references) |
**CRITICAL**: Only add `references` to prompt frontmatter if files are ACTUALLY SAVED to `refs/` directory.
## File Saving
**If user provides file path**:
1. Copy to `refs/ref-NN-{slug}.{ext}` (NN = 01, 02, ...)
2. Create description: `refs/ref-NN-{slug}.md`
3. Verify files exist before proceeding
**Description File Format**:
```yaml
---
ref_id: NN
filename: ref-NN-{slug}.{ext}
usage: direct | style | palette
---
[User's description or auto-generated description]
```
| Usage | When to Use |
|-------|-------------|
| `direct` | Reference matches desired output closely |
| `style` | Extract visual style characteristics only |
| `palette` | Extract color scheme only |
## Verbal Extraction (No File)
When user can't provide file path:
1. Analyze image visually, extract: colors, style, composition
2. Create `refs/extracted-style.md` with extracted info
3. DO NOT add `references` to prompt frontmatter
4. Append extracted style/colors directly to prompt text
## Deep Analysis ⚠️ CRITICAL
References are high-priority inputs. Extract **specific, concrete, reproducible** elements:
| Analysis | Description | Example (good vs bad) |
|----------|-------------|----------------------|
| **Brand elements** | Logos, wordmarks, specific typography | Good: "Logo uses vertical parallel lines for 'm'" / Bad: "Has a logo" |
| **Signature patterns** | Unique decorative motifs, textures | Good: "Woven intersecting curves forming diamond grid" / Bad: "Has patterns" |
| **Color palette** | Exact hex values for key colors | Good: "#2D4A3E dark teal, #F5F0E0 cream" / Bad: "Dark and light colors" |
| **Layout structure** | Specific spatial arrangement | Good: "Bottom 30% dark banner with branding" / Bad: "Has a banner" |
| **Typography** | Font style, weight, spacing, case | Good: "Uppercase, wide letter-spacing" / Bad: "Has text" |
| **Content/subject** | What the reference depicts | Factual description |
| **Usage recommendation** | `direct` / `style` / `palette` | Based on analysis |
**Output format**: List each element as bullet that can be copy-pasted into prompt as mandatory instruction.
## Verification Output
**For saved files**:
```
Reference Images Saved:
- ref-01-{slug}.png ✓ (can use --ref)
- ref-02-{slug}.png ✓ (can use --ref)
```
**For extracted style**:
```
Reference Style Extracted (no file):
- Colors: #E8756D coral, #7ECFC0 mint...
- Style: minimal flat vector, clean lines...
→ Will append to prompt text (not --ref)
```
## Priority Rules
When user provides references, they are **HIGH PRIORITY**:
- **References override defaults**: If reference conflicts with preferred palette/rendering, reference takes precedence
- **Concrete > abstract**: Extract specific elements — not vague "clean style"
- **Mandatory language**: Use "MUST", "REQUIRED" in prompt for reference elements
- **Visible in output**: Verify elements are present after generation; strengthen prompt if not
+24 -18
View File
@@ -14,7 +14,8 @@ Text/image generation via Gemini Web API. Supports reference images and multi-tu
**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
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 | Purpose |
@@ -43,16 +44,20 @@ Before first use, verify user consent for reverse-engineered API usage.
## Preferences (EXTEND.md)
Use Bash to check EXTEND.md existence (priority order):
Check EXTEND.md existence (priority order):
```bash
# Check project-level first
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-danger-gemini-web/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-danger-gemini-web/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md") { "user" }
```
┌──────────────────────────────────────────────────────────┬───────────────────┐
│ Path │ Location │
├──────────────────────────────────────────────────────────┼───────────────────┤
@@ -75,23 +80,23 @@ test -f "$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md" && echo "user"
```bash
# Text generation
npx -y bun ${SKILL_DIR}/scripts/main.ts "Your prompt"
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Your prompt" --model gemini-2.5-pro
${BUN_X} ${SKILL_DIR}/scripts/main.ts "Your prompt"
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Your prompt" --model gemini-3-flash
# Image generation
npx -y bun ${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 --prompt "A cute cat" --image cat.png
${BUN_X} ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
# Vision input (reference images)
npx -y bun ${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 "Describe this" --reference image.png
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Create variation" --reference a.png --image out.png
# Multi-turn conversation
npx -y bun ${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 "Remember: 42" --sessionId session-abc
${BUN_X} ${SKILL_DIR}/scripts/main.ts "What number?" --sessionId session-abc
# JSON output
npx -y bun ${SKILL_DIR}/scripts/main.ts "Hello" --json
${BUN_X} ${SKILL_DIR}/scripts/main.ts "Hello" --json
```
## Options
@@ -100,7 +105,7 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts "Hello" --json
|--------|-------------|
| `--prompt`, `-p` | Prompt text |
| `--promptfiles` | Read prompt from files (concatenated) |
| `--model`, `-m` | Model: gemini-3-pro (default), gemini-2.5-pro, gemini-2.5-flash |
| `--model`, `-m` | Model: gemini-3-pro (default), gemini-3-flash, gemini-3-flash-thinking, gemini-3.1-pro-preview |
| `--image [path]` | Generate image (default: generated.png) |
| `--reference`, `--ref` | Reference images for vision input |
| `--sessionId` | Session ID for multi-turn conversation |
@@ -114,9 +119,10 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts "Hello" --json
| Model | Description |
|-------|-------------|
| `gemini-3-pro` | Default, latest |
| `gemini-2.5-pro` | Previous pro |
| `gemini-2.5-flash` | Fast, lightweight |
| `gemini-3-pro` | Default, latest 3.0 Pro |
| `gemini-3-flash` | Fast, lightweight 3.0 Flash |
| `gemini-3-flash-thinking` | 3.0 Flash with thinking |
| `gemini-3.1-pro-preview` | 3.1 Pro preview (empty header, auto-routed) |
## Authentication
@@ -47,17 +47,22 @@ export class Model {
static readonly UNSPECIFIED = new Model('unspecified', {}, false);
static readonly G_3_0_PRO = new Model(
'gemini-3.0-pro',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]' },
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4],null,null,1]' },
false,
);
static readonly G_2_5_PRO = new Model(
'gemini-2.5-pro',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]' },
static readonly G_3_0_FLASH = new Model(
'gemini-3.0-flash',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"fbb127bbb056c959",null,null,0,[4],null,null,1]' },
false,
);
static readonly G_2_5_FLASH = new Model(
'gemini-2.5-flash',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]' },
static readonly G_3_0_FLASH_THINKING = new Model(
'gemini-3.0-flash-thinking',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"5bf011840784117a",null,null,0,[4],null,null,1]' },
false,
);
static readonly G_3_1_PRO_PREVIEW = new Model(
'gemini-3.1-pro-preview',
{},
false,
);
@@ -68,12 +73,12 @@ export class Model {
) {}
static from_name(name: string): Model {
for (const model of [Model.UNSPECIFIED, Model.G_3_0_PRO, Model.G_2_5_PRO, Model.G_2_5_FLASH]) {
for (const model of [Model.UNSPECIFIED, Model.G_3_0_PRO, Model.G_3_0_FLASH, Model.G_3_0_FLASH_THINKING, Model.G_3_1_PRO_PREVIEW]) {
if (model.model_name === name) return model;
}
throw new Error(
`Unknown model name: ${name}. Available models: ${[Model.UNSPECIFIED, Model.G_3_0_PRO, Model.G_2_5_PRO, Model.G_2_5_FLASH]
`Unknown model name: ${name}. Available models: ${[Model.UNSPECIFIED, Model.G_3_0_PRO, Model.G_3_0_FLASH, Model.G_3_0_FLASH_THINKING, Model.G_3_1_PRO_PREVIEW]
.map((m) => m.model_name)
.join(', ')}`,
);
@@ -91,6 +91,8 @@ class CdpConnection {
}
async function get_free_port(): Promise<number> {
const fixed = parseInt(process.env.GEMINI_WEB_DEBUG_PORT || '', 10);
if (fixed > 0) return fixed;
return await new Promise((resolve, reject) => {
const srv = net.createServer();
srv.unref();
@@ -1,3 +1,4 @@
import { execSync } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
@@ -29,10 +30,23 @@ export function resolveGeminiWebCookiePath(): string {
return path.join(resolveGeminiWebDataDir(), COOKIE_FILE_NAME);
}
let _wslHome: string | null | undefined;
function getWslWindowsHome(): string | null {
if (_wslHome !== undefined) return _wslHome;
if (!process.env.WSL_DISTRO_NAME) { _wslHome = null; return null; }
try {
const raw = execSync('cmd.exe /C "echo %USERPROFILE%"', { encoding: 'utf-8', timeout: 5000 }).trim().replace(/\r/g, '');
_wslHome = execSync(`wslpath -u "${raw}"`, { encoding: 'utf-8', timeout: 5000 }).trim() || null;
} catch { _wslHome = null; }
return _wslHome;
}
export function resolveGeminiWebChromeProfileDir(): string {
const override = process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim();
const override = process.env.BAOYU_CHROME_PROFILE_DIR?.trim() || process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim();
if (override) return path.resolve(override);
return path.join(resolveGeminiWebDataDir(), PROFILE_DIR_NAME);
const wslHome = getWslWindowsHome();
if (wslHome) return path.join(wslHome, '.local', 'share', APP_DATA_DIR, PROFILE_DIR_NAME);
return path.join(resolveUserDataRoot(), APP_DATA_DIR, PROFILE_DIR_NAME);
}
export function resolveGeminiWebSessionsDir(): string {
@@ -73,7 +73,7 @@ Multi-turn conversation (agent generates unique sessionId):
Options:
-p, --prompt <text> Prompt text
--promptfiles <files...> Read prompt from one or more files (concatenated in order)
-m, --model <id> gemini-3-pro | gemini-2.5-pro | gemini-2.5-flash (default: gemini-3-pro)
-m, --model <id> gemini-3-pro | gemini-3-flash | gemini-3-flash-thinking | gemini-3.1-pro-preview (default: gemini-3-pro)
--json Output JSON
--image [path] Generate an image and save it (default: ./generated.png)
--reference <files...> Reference images for vision input
@@ -227,8 +227,11 @@ function resolveModel(id: string): Model {
const k = id.trim();
if (k === 'gemini-3-pro') return Model.G_3_0_PRO;
if (k === 'gemini-3.0-pro') return Model.G_3_0_PRO;
if (k === 'gemini-2.5-pro') return Model.G_2_5_PRO;
if (k === 'gemini-2.5-flash') return Model.G_2_5_FLASH;
if (k === 'gemini-3-flash') return Model.G_3_0_FLASH;
if (k === 'gemini-3.0-flash') return Model.G_3_0_FLASH;
if (k === 'gemini-3-flash-thinking') return Model.G_3_0_FLASH_THINKING;
if (k === 'gemini-3.0-flash-thinking') return Model.G_3_0_FLASH_THINKING;
if (k === 'gemini-3.1-pro-preview') return Model.G_3_1_PRO_PREVIEW;
return Model.from_name(k);
}
+81 -12
View File
@@ -16,6 +16,7 @@ Scripts located in `scripts/` subdirectory.
**Path Resolution**:
1. `SKILL_DIR` = this SKILL.md's directory
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
@@ -69,16 +70,20 @@ Use `AskUserQuestion` with options: "Yes, I accept" | "No, I decline"
## Preferences (EXTEND.md)
Use Bash to check EXTEND.md existence (priority order):
Check EXTEND.md existence (priority order):
```bash
# Check project-level first
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md") { "user" }
```
┌────────────────────────────────────────────────────────────┬───────────────────┐
│ Path │ Location │
├────────────────────────────────────────────────────────────┼───────────────────┤
@@ -92,17 +97,53 @@ test -f "$HOME/.baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md" && echo "user
├───────────┼───────────────────────────────────────────────────────────────────────────┤
│ 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 | Output format preferences
**EXTEND.md Supports**: Download media by default | Default output directory
### 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 tweets?"
- "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?"
- "x-to-markdown (Recommended)" — Save to ./x-to-markdown/{username}/{tweet-id}.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 = `./x-to-markdown/`) |
**Value priority**:
1. CLI arguments (`--download-media`, `-o`)
2. EXTEND.md
3. Skill defaults
## Usage
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts <url>
npx -y bun ${SKILL_DIR}/scripts/main.ts <url> -o output.md
npx -y bun ${SKILL_DIR}/scripts/main.ts <url> --json
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url>
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> -o output.md
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --download-media
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --json
```
## Options
@@ -112,6 +153,7 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts <url> --json
| `<url>` | Tweet or article URL |
| `-o <path>` | Output path |
| `--json` | JSON output |
| `--download-media` | Download image/video assets to local `imgs/` and `videos/`, and rewrite markdown links to local relative paths |
| `--login` | Refresh cookies only |
## Supported URLs
@@ -124,15 +166,42 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts <url> --json
```markdown
---
url: https://x.com/user/status/123
url: "https://x.com/user/status/123"
author: "Name (@user)"
tweet_count: 3
tweetCount: 3
coverImage: "https://pbs.twimg.com/media/example.jpg"
---
Content...
```
**File structure**: `x-to-markdown/{username}/{tweet-id}.md`
**File structure**: `x-to-markdown/{username}/{tweet-id}/{content-slug}.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)
## Authentication
@@ -0,0 +1,106 @@
---
name: first-time-setup
description: First-time setup flow for baoyu-danger-x-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 tweets or articles
- 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 tweets?"
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: "x-to-markdown (Recommended)"
description: "Save to ./x-to-markdown/{username}/{tweet-id}.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-danger-x-to-markdown/EXTEND.md` | All projects |
| Project | `.baoyu-skills/baoyu-danger-x-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.
@@ -117,6 +117,8 @@ class CdpConnection {
}
async function getFreePort(): Promise<number> {
const fixed = parseInt(process.env.X_DEBUG_PORT || "", 10);
if (fixed > 0) return fixed;
return await new Promise((resolve, reject) => {
const srv = net.createServer();
srv.unref();
+237 -30
View File
@@ -2,10 +2,12 @@ import fs from "node:fs";
import path from "node:path";
import readline from "node:readline";
import process from "node:process";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { fetchXArticle } from "./graphql.js";
import { formatArticleMarkdown } from "./markdown.js";
import { localizeMarkdownMedia, type LocalizeMarkdownMediaResult } from "./media-localizer.js";
import { resolveReferencedTweetsFromArticle } from "./referenced-tweets.js";
import { hasRequiredXCookies, loadXCookies, refreshXCookies } from "./cookies.js";
import { resolveXToMarkdownConsentPath } from "./paths.js";
import { tweetToMarkdown } from "./tweet-to-markdown.js";
@@ -15,6 +17,7 @@ type CliArgs = {
output: string | null;
json: boolean;
login: boolean;
downloadMedia: boolean;
help: boolean;
};
@@ -38,6 +41,7 @@ Usage:
Options:
--output <path>, -o Output path (file or dir). Default: ./x-to-markdown/<slug>/
--json Output as JSON
--download-media Download images/videos to local ./imgs and ./videos next to markdown
--login Refresh cookies only, then exit
--help, -h Show help
@@ -45,6 +49,7 @@ Examples:
${cmd} https://x.com/username/status/1234567890
${cmd} https://x.com/i/article/1234567890 -o ./article.md
${cmd} https://x.com/username/status/1234567890 -o ./out/
${cmd} https://x.com/username/status/1234567890 --download-media
${cmd} https://x.com/username/status/1234567890 --json | jq -r '.markdownPath'
${cmd} --login
`);
@@ -57,6 +62,7 @@ function parseArgs(argv: string[]): CliArgs {
output: null,
json: false,
login: false,
downloadMedia: false,
help: false,
};
@@ -80,6 +86,11 @@ function parseArgs(argv: string[]): CliArgs {
continue;
}
if (a === "--download-media") {
out.downloadMedia = true;
continue;
}
if (a === "--url") {
const v = argv[++i];
if (!v) throw new Error("Missing value for --url");
@@ -172,36 +183,160 @@ function sanitizeSlug(input: string): string {
.slice(0, 120);
}
function formatBackupTimestamp(date: Date = new Date()): string {
const pad2 = (n: number) => String(n).padStart(2, "0");
return `${date.getFullYear()}${pad2(date.getMonth() + 1)}${pad2(date.getDate())}-${pad2(date.getHours())}${pad2(
date.getMinutes()
)}${pad2(date.getSeconds())}`;
function extractContentSlug(markdown: string): string {
const headingMatch = markdown.match(/^#\s+(.+)$/m);
if (headingMatch?.[1]) {
return sanitizeSlug(headingMatch[1].slice(0, 60)).toLowerCase();
}
const lines = markdown.split("\n");
let inFrontmatter = false;
for (const line of lines) {
if (line === "---") {
inFrontmatter = !inFrontmatter;
continue;
}
if (inFrontmatter) continue;
const trimmed = line.trim();
if (trimmed) {
return sanitizeSlug(trimmed.slice(0, 60)).toLowerCase();
}
}
return "untitled";
}
async function backupDirIfExists(dir: string, log: (message: string) => void): Promise<void> {
function resolveSlugAndId(normalizedUrl: string, kind: "tweet" | "article"): { slug: string; idPart: string } {
const articleId = kind === "article" ? parseArticleId(normalizedUrl) : null;
const tweetId = kind === "tweet" ? parseTweetId(normalizedUrl) : null;
const username = kind === "tweet" ? parseTweetUsername(normalizedUrl) : null;
const idPart = articleId ?? tweetId ?? String(Date.now());
const userSlug = username ? sanitizeSlug(username) : null;
const slug = userSlug ?? idPart;
return { slug, idPart };
}
function extractFrontmatterUrls(markdown: string): string[] {
const match = markdown.match(/^---\n([\s\S]*?)\n---/);
if (!match?.[1]) return [];
const lines = match[1].split("\n");
const urls: string[] = [];
for (const line of lines) {
const m = line.match(/^(url|requestedUrl):\s*["']([^"']+)["']\s*$/);
if (m?.[2]) {
urls.push(m[2]);
}
}
return urls;
}
function frontmatterMatchesTarget(
markdown: string,
normalizedUrl: string,
kind: "tweet" | "article"
): boolean {
const urls = extractFrontmatterUrls(markdown);
if (urls.length === 0) return false;
const targetId = kind === "article" ? parseArticleId(normalizedUrl) : parseTweetId(normalizedUrl);
if (!targetId) return false;
for (const url of urls) {
const candidateId = kind === "article" ? parseArticleId(url) : parseTweetId(url);
if (candidateId && candidateId === targetId) {
return true;
}
}
return false;
}
function listMarkdownFiles(dirPath: string): string[] {
try {
if (!fs.existsSync(dir)) return;
const stat = fs.statSync(dir);
if (!stat.isDirectory()) return;
const backup = `${dir}-backup-${formatBackupTimestamp()}`;
await rename(dir, backup);
log(`[x-to-markdown] Existing directory moved to: ${backup}`);
} catch (error) {
throw new Error(
`Failed to backup existing directory (${dir}): ${error instanceof Error ? error.message : String(error ?? "")}`
);
return fs
.readdirSync(dirPath)
.filter((name) => name.toLowerCase().endsWith(".md"))
.map((name) => path.join(dirPath, name))
.sort();
} catch {
return [];
}
}
function resolveDefaultOutputDir(slug: string): string {
return path.resolve(process.cwd(), "x-to-markdown", slug);
function resolveExistingMarkdownPath(
normalizedUrl: string,
kind: "tweet" | "article",
argsOutput: string | null
): string | null {
const { slug, idPart } = resolveSlugAndId(normalizedUrl, kind);
const candidateDirs = new Set<string>();
const candidateFiles = new Set<string>();
if (argsOutput) {
const resolved = path.resolve(argsOutput);
const looksDir = argsOutput.endsWith("/") || argsOutput.endsWith("\\");
try {
if (fs.existsSync(resolved)) {
const stat = fs.statSync(resolved);
if (stat.isFile()) {
candidateFiles.add(resolved);
} else if (stat.isDirectory()) {
candidateDirs.add(path.join(resolved, slug, idPart));
candidateDirs.add(resolved);
}
} else if (looksDir) {
candidateDirs.add(path.join(resolved, slug, idPart));
}
} catch {
// ignore and continue
}
} else {
candidateDirs.add(path.resolve(process.cwd(), "x-to-markdown", slug, idPart));
}
for (const filePath of candidateFiles) {
if (!filePath.toLowerCase().endsWith(".md")) continue;
try {
const markdown = fs.readFileSync(filePath, "utf8");
if (frontmatterMatchesTarget(markdown, normalizedUrl, kind)) {
return filePath;
}
} catch {
// ignore and continue
}
}
for (const dirPath of candidateDirs) {
if (!fs.existsSync(dirPath)) continue;
let stat: fs.Stats;
try {
stat = fs.statSync(dirPath);
} catch {
continue;
}
if (!stat.isDirectory()) continue;
const markdownFiles = listMarkdownFiles(dirPath);
for (const filePath of markdownFiles) {
try {
const markdown = fs.readFileSync(filePath, "utf8");
if (frontmatterMatchesTarget(markdown, normalizedUrl, kind)) {
return filePath;
}
} catch {
// ignore and continue
}
}
}
return null;
}
async function resolveOutputPath(
normalizedUrl: string,
kind: "tweet" | "article",
argsOutput: string | null,
contentSlug: string,
log: (message: string) => void
): Promise<{ outputDir: string; markdownPath: string; slug: string }> {
const articleId = kind === "article" ? parseArticleId(normalizedUrl) : null;
@@ -212,15 +347,14 @@ async function resolveOutputPath(
const idPart = articleId ?? tweetId ?? String(Date.now());
const slug = userSlug ?? idPart;
const defaultFileName = kind === "article" ? `${idPart}.md` : `${idPart}.md`;
const defaultFileName = `${contentSlug}.md`;
if (argsOutput) {
const wantsDir = argsOutput.endsWith("/") || argsOutput.endsWith("\\");
const resolved = path.resolve(argsOutput);
try {
if (wantsDir || (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory())) {
const outputDir = path.join(resolved, slug);
await backupDirIfExists(outputDir, log);
const outputDir = path.join(resolved, slug, idPart);
await mkdir(outputDir, { recursive: true });
return { outputDir, markdownPath: path.join(outputDir, defaultFileName), slug };
}
@@ -233,8 +367,7 @@ async function resolveOutputPath(
return { outputDir, markdownPath: resolved, slug };
}
const outputDir = resolveDefaultOutputDir(slug);
await backupDirIfExists(outputDir, log);
const outputDir = path.resolve(process.cwd(), "x-to-markdown", slug, idPart);
await mkdir(outputDir, { recursive: true });
return { outputDir, markdownPath: path.join(outputDir, defaultFileName), slug };
}
@@ -345,16 +478,18 @@ async function convertArticleToMarkdown(
log(`[x-to-markdown] Fetching article ${articleId}...`);
const article = await fetchXArticle(articleId, cookieMap, false);
const body = formatArticleMarkdown(article).trimEnd();
const referencedTweets = await resolveReferencedTweetsFromArticle(article, cookieMap, { log });
const { markdown: body, coverUrl } = formatArticleMarkdown(article, { referencedTweets });
const title = typeof (article as any)?.title === "string" ? String((article as any).title).trim() : "";
const meta = formatMetaMarkdown({
url: `https://x.com/i/article/${articleId}`,
requested_url: inputUrl,
requestedUrl: inputUrl,
title: title || null,
coverImage: coverUrl,
});
return [meta, body].filter(Boolean).join("\n\n").trimEnd();
return [meta, body.trimEnd()].filter(Boolean).join("\n\n").trimEnd();
}
async function main(): Promise<void> {
@@ -383,13 +518,80 @@ async function main(): Promise<void> {
}
const kind = articleId ? ("article" as const) : ("tweet" as const);
const { outputDir, markdownPath, slug } = await resolveOutputPath(normalizedUrl, kind, args.output, log);
const markdown =
if (args.downloadMedia) {
const existingMarkdownPath = resolveExistingMarkdownPath(normalizedUrl, kind, args.output);
if (existingMarkdownPath) {
log(`[x-to-markdown] Reusing existing markdown: ${existingMarkdownPath}`);
const existingMarkdown = await readFile(existingMarkdownPath, "utf8");
const mediaResult = await localizeMarkdownMedia(existingMarkdown, {
markdownPath: existingMarkdownPath,
log,
});
const didLocalize =
mediaResult.downloadedImages > 0 ||
mediaResult.downloadedVideos > 0 ||
mediaResult.markdown !== existingMarkdown;
if (didLocalize) {
await writeFile(existingMarkdownPath, mediaResult.markdown, "utf8");
log(
`[x-to-markdown] Media localized: images=${mediaResult.downloadedImages}, videos=${mediaResult.downloadedVideos}`
);
log(`[x-to-markdown] Saved: ${existingMarkdownPath}`);
const { slug } = resolveSlugAndId(normalizedUrl, kind);
if (args.json) {
console.log(
JSON.stringify(
{
url: articleId ? `https://x.com/i/article/${articleId}` : normalizedUrl,
requestedUrl: normalizedUrl,
type: kind,
slug,
outputDir: path.dirname(existingMarkdownPath),
markdownPath: existingMarkdownPath,
downloadMedia: true,
downloadedImages: mediaResult.downloadedImages,
downloadedVideos: mediaResult.downloadedVideos,
imageDir: mediaResult.imageDir,
videoDir: mediaResult.videoDir,
},
null,
2
)
);
} else {
console.log(existingMarkdownPath);
}
return;
}
log("[x-to-markdown] Existing markdown already localized; rebuilding content to refresh placement.");
}
}
let markdown =
kind === "article" && articleId
? await convertArticleToMarkdown(normalizedUrl, articleId, log)
: await tweetToMarkdown(normalizedUrl, { log });
const contentSlug = extractContentSlug(markdown);
const { outputDir, markdownPath, slug } = await resolveOutputPath(normalizedUrl, kind, args.output, contentSlug, log);
let mediaResult: LocalizeMarkdownMediaResult | null = null;
if (args.downloadMedia) {
mediaResult = await localizeMarkdownMedia(markdown, {
markdownPath,
log,
});
markdown = mediaResult.markdown;
log(
`[x-to-markdown] Media localized: images=${mediaResult.downloadedImages}, videos=${mediaResult.downloadedVideos}`
);
}
await writeFile(markdownPath, markdown, "utf8");
log(`[x-to-markdown] Saved: ${markdownPath}`);
@@ -398,11 +600,16 @@ async function main(): Promise<void> {
JSON.stringify(
{
url: articleId ? `https://x.com/i/article/${articleId}` : normalizedUrl,
requested_url: normalizedUrl,
requestedUrl: normalizedUrl,
type: kind,
slug,
outputDir,
markdownPath,
downloadMedia: args.downloadMedia,
downloadedImages: mediaResult?.downloadedImages ?? 0,
downloadedVideos: mediaResult?.downloadedVideos ?? 0,
imageDir: mediaResult?.imageDir ?? null,
videoDir: mediaResult?.videoDir ?? null,
},
null,
2
@@ -2,9 +2,22 @@ import type {
ArticleBlock,
ArticleContentState,
ArticleEntity,
ArticleEntityMapEntry,
ArticleMediaInfo,
} from "./types.js";
export type ReferencedTweetInfo = {
id: string;
url: string;
authorName?: string;
authorUsername?: string;
text?: string;
};
export type FormatArticleOptions = {
referencedTweets?: Map<string, ReferencedTweetInfo>;
};
function coerceArticleEntity(value: unknown): ArticleEntity | null {
if (!value || typeof value !== "object") return null;
const candidate = value as ArticleEntity;
@@ -29,6 +42,73 @@ function normalizeCaption(caption?: string): string {
return trimmed.replace(/\s+/g, " ");
}
function summarizeTweetText(text?: string): string {
const trimmed = text?.trim();
if (!trimmed) return "";
const normalized = trimmed
.split(/\r?\n+/)
.map((line) => line.trim())
.filter(Boolean)
.join(" ");
if (normalized.length <= 280) return normalized;
return `${normalized.slice(0, 277)}...`;
}
function buildTweetUrl(tweetId?: string, username?: string): string | null {
if (!tweetId) return null;
if (username) {
return `https://x.com/${username}/status/${tweetId}`;
}
return `https://x.com/i/web/status/${tweetId}`;
}
type EntityLookup = {
byIndex: Map<number, ArticleEntityMapEntry>;
byLogicalKey: Map<number, ArticleEntityMapEntry>;
};
function buildEntityLookup(
entityMap: ArticleContentState["entityMap"] | undefined
): EntityLookup {
const lookup: EntityLookup = {
byIndex: new Map<number, ArticleEntityMapEntry>(),
byLogicalKey: new Map<number, ArticleEntityMapEntry>(),
};
if (!entityMap) return lookup;
for (const [idx, entry] of Object.entries(entityMap)) {
const idxNum = Number(idx);
if (Number.isFinite(idxNum)) {
lookup.byIndex.set(idxNum, entry);
}
const logicalKey = parseInt(entry?.key ?? "", 10);
if (Number.isFinite(logicalKey) && !lookup.byLogicalKey.has(logicalKey)) {
lookup.byLogicalKey.set(logicalKey, entry);
}
}
return lookup;
}
function resolveEntityEntry(
entityKey: number | undefined,
entityMap: ArticleContentState["entityMap"] | undefined,
lookup: EntityLookup
): ArticleEntityMapEntry | undefined {
if (entityKey === undefined) return undefined;
const byLogicalKey = lookup.byLogicalKey.get(entityKey);
if (byLogicalKey) return byLogicalKey;
const byIndex = lookup.byIndex.get(entityKey);
if (byIndex) return byIndex;
if (!entityMap) return undefined;
return entityMap[String(entityKey)];
}
function resolveMediaUrl(info?: ArticleMediaInfo): string | undefined {
if (!info) return undefined;
if (info.original_img_url) return info.original_img_url;
@@ -79,11 +159,12 @@ function collectMediaUrls(
function resolveEntityMediaLines(
entityKey: number | undefined,
entityMap: ArticleContentState["entityMap"] | undefined,
entityLookup: EntityLookup,
mediaById: Map<string, string>,
usedUrls: Set<string>
): string[] {
if (entityKey === undefined || !entityMap) return [];
const entry = entityMap[String(entityKey)];
if (entityKey === undefined) return [];
const entry = resolveEntityEntry(entityKey, entityMap, entityLookup);
const value = entry?.value;
if (!value) return [];
const type = value.type;
@@ -117,11 +198,142 @@ function resolveEntityMediaLines(
return lines;
}
function resolveEntityTweetLines(
entityKey: number | undefined,
entityMap: ArticleContentState["entityMap"] | undefined,
entityLookup: EntityLookup,
referencedTweets?: Map<string, ReferencedTweetInfo>
): string[] {
if (entityKey === undefined) return [];
const entry = resolveEntityEntry(entityKey, entityMap, entityLookup);
const value = entry?.value;
if (!value || value.type !== "TWEET") return [];
const tweetId = typeof value.data?.tweetId === "string" ? value.data.tweetId : "";
if (!tweetId) return [];
const referenced = referencedTweets?.get(tweetId);
const url =
referenced?.url ??
buildTweetUrl(tweetId, referenced?.authorUsername) ??
`https://x.com/i/web/status/${tweetId}`;
const authorText =
referenced?.authorName && referenced?.authorUsername
? `${referenced.authorName} (@${referenced.authorUsername})`
: referenced?.authorUsername
? `@${referenced.authorUsername}`
: referenced?.authorName;
const lines: string[] = [];
lines.push(`> 引用推文${authorText ? `${authorText}` : ""}`);
const summary = summarizeTweetText(referenced?.text);
if (summary) {
lines.push(`> ${summary}`);
}
lines.push(`> ${url}`);
return lines;
}
function buildMediaLinkMap(
entityMap: ArticleContentState["entityMap"] | undefined
): Map<number, string> {
const map = new Map<number, string>();
if (!entityMap) return map;
const mediaEntries: { idx: number; key: number }[] = [];
const linkEntries: { key: number; url: string }[] = [];
for (const [idx, entry] of Object.entries(entityMap)) {
const value = entry?.value;
if (!value) continue;
const key = parseInt(entry?.key ?? "", 10);
if (isNaN(key)) continue;
if (value.type === "MEDIA" || value.type === "IMAGE") {
mediaEntries.push({ idx: Number(idx), key });
} else if (value.type === "LINK" && typeof value.data?.url === "string") {
linkEntries.push({ key, url: value.data.url });
}
}
if (mediaEntries.length === 0 || linkEntries.length === 0) return map;
mediaEntries.sort((a, b) => a.key - b.key);
linkEntries.sort((a, b) => a.key - b.key);
const pool = [...linkEntries];
for (const media of mediaEntries) {
if (pool.length === 0) break;
let linkIdx = pool.findIndex((l) => l.key > media.key);
if (linkIdx === -1) linkIdx = 0;
const link = pool.splice(linkIdx, 1)[0]!;
map.set(media.idx, link.url);
map.set(media.key, link.url);
}
return map;
}
function renderInlineLinks(
text: string,
entityRanges: Array<{ key?: number; offset?: number; length?: number }>,
entityMap: ArticleContentState["entityMap"] | undefined,
entityLookup: EntityLookup,
mediaLinkMap: Map<number, string>
): string {
if (!entityMap || entityRanges.length === 0) return text;
const valid = entityRanges.filter(
(r) =>
typeof r.key === "number" &&
typeof r.offset === "number" &&
typeof r.length === "number" &&
r.length > 0
);
if (valid.length === 0) return text;
const sorted = [...valid].sort((a, b) => (b.offset ?? 0) - (a.offset ?? 0));
let result = text;
for (const range of sorted) {
const offset = range.offset!;
const length = range.length!;
const key = range.key!;
const entry = resolveEntityEntry(key, entityMap, entityLookup);
const value = entry?.value;
if (!value) continue;
let url: string | undefined;
if (value.type === "LINK" && typeof value.data?.url === "string") {
url = value.data.url;
} else if (value.type === "MEDIA" || value.type === "IMAGE") {
url = mediaLinkMap.get(key);
}
if (!url) continue;
const linkText = result.slice(offset, offset + length);
result =
result.slice(0, offset) +
`[${linkText}](${url})` +
result.slice(offset + length);
}
return result;
}
function renderContentBlocks(
blocks: ArticleBlock[],
entityMap: ArticleContentState["entityMap"] | undefined,
entityLookup: EntityLookup,
mediaById: Map<string, string>,
usedUrls: Set<string>
usedUrls: Set<string>,
mediaLinkMap: Map<number, string>,
referencedTweets?: Map<string, ReferencedTweetInfo>
): string[] {
const lines: string[] = [];
let previousKind: "list" | "quote" | "heading" | "text" | "code" | "media" | null = null;
@@ -150,14 +362,55 @@ function renderContentBlocks(
const mediaLines: string[] = [];
for (const range of ranges) {
if (typeof range?.key !== "number") continue;
mediaLines.push(...resolveEntityMediaLines(range.key, entityMap, mediaById, usedUrls));
mediaLines.push(
...resolveEntityMediaLines(range.key, entityMap, entityLookup, mediaById, usedUrls)
);
}
return mediaLines;
};
const collectTweetLines = (block: ArticleBlock): string[] => {
const ranges = Array.isArray(block.entityRanges) ? block.entityRanges : [];
const tweetLines: string[] = [];
for (const range of ranges) {
if (typeof range?.key !== "number") continue;
tweetLines.push(
...resolveEntityTweetLines(range.key, entityMap, entityLookup, referencedTweets)
);
}
return tweetLines;
};
const collectLinkLines = (block: ArticleBlock): string[] => {
const ranges = Array.isArray(block.entityRanges) ? block.entityRanges : [];
const linkLines: string[] = [];
for (const range of ranges) {
if (typeof range?.key !== "number") continue;
const entry = resolveEntityEntry(range.key, entityMap, entityLookup);
const value = entry?.value;
if (value?.type !== "LINK") continue;
const url = typeof value.data?.url === "string" ? value.data.url : "";
if (url) {
linkLines.push(url);
}
}
return [...new Set(linkLines)];
};
const pushTrailingMedia = (mediaLines: string[]) => {
if (mediaLines.length > 0) {
pushBlock(mediaLines, "media");
}
};
for (const block of blocks) {
const type = typeof block?.type === "string" ? block.type : "unstyled";
const text = typeof block?.text === "string" ? block.text : "";
const rawText = typeof block?.text === "string" ? block.text : "";
const ranges = Array.isArray(block.entityRanges) ? block.entityRanges : [];
const text =
type !== "atomic" && type !== "code-block"
? renderInlineLinks(rawText, ranges, entityMap, entityLookup, mediaLinkMap)
: rawText;
if (type === "code-block") {
if (!inCodeBlock) {
@@ -182,10 +435,22 @@ function renderContentBlocks(
}
listKind = null;
orderedIndex = 0;
const tweetLines = collectTweetLines(block);
if (tweetLines.length > 0) {
pushBlock(tweetLines, "quote");
}
const mediaLines = collectMediaLines(block);
if (mediaLines.length > 0) {
pushBlock(mediaLines, "media");
}
const linkLines = collectLinkLines(block);
if (linkLines.length > 0) {
pushBlock(linkLines, "text");
}
continue;
}
@@ -199,6 +464,7 @@ function renderContentBlocks(
listKind = "unordered";
orderedIndex = 0;
pushBlock([`- ${text}`], "list");
pushTrailingMedia(collectMediaLines(block));
continue;
}
@@ -209,6 +475,7 @@ function renderContentBlocks(
listKind = "ordered";
orderedIndex += 1;
pushBlock([`${orderedIndex}. ${text}`], "list");
pushTrailingMedia(collectMediaLines(block));
continue;
}
@@ -218,36 +485,43 @@ function renderContentBlocks(
switch (type) {
case "header-one":
pushBlock([`# ${text}`], "heading");
pushTrailingMedia(collectMediaLines(block));
break;
case "header-two":
pushBlock([`## ${text}`], "heading");
pushTrailingMedia(collectMediaLines(block));
break;
case "header-three":
pushBlock([`### ${text}`], "heading");
pushTrailingMedia(collectMediaLines(block));
break;
case "header-four":
pushBlock([`#### ${text}`], "heading");
pushTrailingMedia(collectMediaLines(block));
break;
case "header-five":
pushBlock([`##### ${text}`], "heading");
pushTrailingMedia(collectMediaLines(block));
break;
case "header-six":
pushBlock([`###### ${text}`], "heading");
pushTrailingMedia(collectMediaLines(block));
break;
case "blockquote": {
const quoteLines = text.length > 0 ? text.split("\n") : [""];
pushBlock(quoteLines.map((line) => `> ${line}`), "quote");
pushTrailingMedia(collectMediaLines(block));
break;
}
default:
if (/^XIMGPH_\d+$/.test(text.trim())) {
pushTrailingMedia(collectMediaLines(block));
break;
}
pushBlock([text], "text");
pushTrailingMedia(collectMediaLines(block));
break;
}
const trailingMediaLines = collectMediaLines(block);
if (trailingMediaLines.length > 0) {
pushBlock(trailingMediaLines, "media");
}
}
if (inCodeBlock) {
@@ -257,10 +531,36 @@ function renderContentBlocks(
return lines;
}
export function formatArticleMarkdown(article: unknown): string {
export type FormatArticleResult = {
markdown: string;
coverUrl: string | null;
};
export function extractReferencedTweetIds(article: unknown): string[] {
const candidate = coerceArticleEntity(article);
const entityMap = candidate?.content_state?.entityMap;
if (!entityMap) return [];
const ids: string[] = [];
const seen = new Set<string>();
for (const entry of Object.values(entityMap)) {
const value = entry?.value;
if (value?.type !== "TWEET") continue;
const tweetId = typeof value.data?.tweetId === "string" ? value.data.tweetId : "";
if (!tweetId || seen.has(tweetId)) continue;
seen.add(tweetId);
ids.push(tweetId);
}
return ids;
}
export function formatArticleMarkdown(
article: unknown,
options: FormatArticleOptions = {}
): FormatArticleResult {
const candidate = coerceArticleEntity(article);
if (!candidate) {
return `\`\`\`json\n${JSON.stringify(article, null, 2)}\n\`\`\``;
return { markdown: `\`\`\`json\n${JSON.stringify(article, null, 2)}\n\`\`\``, coverUrl: null };
}
const lines: string[] = [];
@@ -271,17 +571,25 @@ export function formatArticleMarkdown(article: unknown): string {
lines.push(`# ${title}`);
}
const coverUrl = resolveMediaUrl(candidate.cover_media?.media_info);
const coverUrl = resolveMediaUrl(candidate.cover_media?.media_info) ?? null;
if (coverUrl) {
if (lines.length > 0) lines.push("");
lines.push(`![](${coverUrl})`);
usedUrls.add(coverUrl);
}
const blocks = candidate.content_state?.blocks;
const entityMap = candidate.content_state?.entityMap;
const entityLookup = buildEntityLookup(entityMap);
if (Array.isArray(blocks) && blocks.length > 0) {
const rendered = renderContentBlocks(blocks, entityMap, mediaById, usedUrls);
const mediaLinkMap = buildMediaLinkMap(entityMap);
const rendered = renderContentBlocks(
blocks,
entityMap,
entityLookup,
mediaById,
usedUrls,
mediaLinkMap,
options.referencedTweets
);
if (rendered.length > 0) {
if (lines.length > 0) lines.push("");
lines.push(...rendered);
@@ -294,7 +602,7 @@ export function formatArticleMarkdown(article: unknown): string {
lines.push(candidate.preview_text.trim());
}
const mediaUrls = collectMediaUrls(candidate, usedUrls, coverUrl);
const mediaUrls = collectMediaUrls(candidate, usedUrls, coverUrl ?? undefined);
if (mediaUrls.length > 0) {
lines.push("", "## Media", "");
for (const url of mediaUrls) {
@@ -302,5 +610,5 @@ export function formatArticleMarkdown(article: unknown): string {
}
}
return lines.join("\n").trimEnd();
return { markdown: lines.join("\n").trimEnd(), coverUrl };
}
@@ -0,0 +1,330 @@
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 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 resolveKindFromHostname(rawUrl: string): MediaKind | undefined {
try {
const hostname = new URL(rawUrl).hostname.toLowerCase();
if (hostname.includes("video.twimg.com")) return "video";
if (hostname.includes("pbs.twimg.com")) return "image";
} catch {
return undefined;
}
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;
const kindFromHost = resolveKindFromHostname(rawUrl);
if (kindFromHost) return kindFromHost;
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}`;
}
const FRONTMATTER_COVER_RE = /^(coverImage:\s*")(https?:\/\/[^"]+)(")/m;
function toHighResUrl(rawUrl: string): string {
try {
const parsed = new URL(rawUrl);
if (parsed.hostname !== "pbs.twimg.com") return rawUrl;
const ext = path.posix.extname(parsed.pathname).replace(/^\./, "").toLowerCase();
if (!ext || !IMAGE_EXTENSIONS.has(ext)) return rawUrl;
parsed.pathname = parsed.pathname.replace(new RegExp(`\\.${ext}$`), "");
parsed.searchParams.set("format", ext === "jpeg" ? "jpg" : ext);
parsed.searchParams.set("name", "4096x4096");
return parsed.toString();
} catch {
return rawUrl;
}
}
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 downloadUrl = toHighResUrl(candidate.url);
const response = await fetch(downloadUrl, {
method: "GET",
redirect: "follow",
headers: {
"user-agent": DOWNLOAD_USER_AGENT,
},
});
if (!response.ok) {
log(`[x-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(`[x-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,
};
}
@@ -1,3 +1,4 @@
import { execSync } from "node:child_process";
import os from "node:os";
import path from "node:path";
import process from "node:process";
@@ -30,10 +31,23 @@ export function resolveXToMarkdownCookiePath(): string {
return path.join(resolveXToMarkdownDataDir(), COOKIE_FILE_NAME);
}
let _wslHome: string | null | undefined;
function getWslWindowsHome(): string | null {
if (_wslHome !== undefined) return _wslHome;
if (!process.env.WSL_DISTRO_NAME) { _wslHome = null; return null; }
try {
const raw = execSync('cmd.exe /C "echo %USERPROFILE%"', { encoding: 'utf-8', timeout: 5000 }).trim().replace(/\r/g, '');
_wslHome = execSync(`wslpath -u "${raw}"`, { encoding: 'utf-8', timeout: 5000 }).trim() || null;
} catch { _wslHome = null; }
return _wslHome;
}
export function resolveXToMarkdownChromeProfileDir(): string {
const override = process.env.X_CHROME_PROFILE_DIR?.trim();
const override = process.env.BAOYU_CHROME_PROFILE_DIR?.trim() || process.env.X_CHROME_PROFILE_DIR?.trim();
if (override) return path.resolve(override);
return path.join(resolveXToMarkdownDataDir(), PROFILE_DIR_NAME);
const wslHome = getWslWindowsHome();
if (wslHome) return path.join(wslHome, ".local", "share", APP_DATA_DIR, PROFILE_DIR_NAME);
return path.join(resolveUserDataRoot(), APP_DATA_DIR, PROFILE_DIR_NAME);
}
export function resolveXToMarkdownConsentPath(): string {
@@ -0,0 +1,81 @@
import { fetchXTweet } from "./graphql.js";
import {
extractReferencedTweetIds,
type ReferencedTweetInfo,
} from "./markdown.js";
type ResolveReferencedTweetsOptions = {
log?: (message: string) => void;
};
function extractReferencedTweetInfo(tweet: any, fallbackTweetId: string): ReferencedTweetInfo {
const userCore = tweet?.core?.user_results?.result?.core;
const userLegacy = tweet?.core?.user_results?.result?.legacy;
const authorName =
typeof userCore?.name === "string"
? userCore.name
: typeof userLegacy?.name === "string"
? userLegacy.name
: undefined;
const authorUsername =
typeof userCore?.screen_name === "string"
? userCore.screen_name
: typeof userLegacy?.screen_name === "string"
? userLegacy.screen_name
: undefined;
const text =
tweet?.note_tweet?.note_tweet_results?.result?.text ??
tweet?.legacy?.full_text ??
tweet?.legacy?.text ??
undefined;
const tweetId =
typeof tweet?.rest_id === "string" && tweet.rest_id.length > 0
? tweet.rest_id
: fallbackTweetId;
const url = authorUsername
? `https://x.com/${authorUsername}/status/${tweetId}`
: `https://x.com/i/web/status/${tweetId}`;
return {
id: tweetId,
url,
authorName,
authorUsername,
text: typeof text === "string" ? text : undefined,
};
}
export async function resolveReferencedTweetsFromArticle(
article: unknown,
cookieMap: Record<string, string>,
options: ResolveReferencedTweetsOptions = {}
): Promise<Map<string, ReferencedTweetInfo>> {
const log = options.log ?? (() => {});
const ids = extractReferencedTweetIds(article);
const referencedTweets = new Map<string, ReferencedTweetInfo>();
for (const id of ids) {
try {
const tweet = await fetchXTweet(id, cookieMap, false);
const info = extractReferencedTweetInfo(tweet, id);
referencedTweets.set(id, info);
} catch (error) {
log(
`[x-to-markdown] Failed to fetch referenced tweet ${id}: ${
error instanceof Error ? error.message : String(error)
}`
);
referencedTweets.set(id, {
id,
url: `https://x.com/i/web/status/${id}`,
});
}
}
return referencedTweets;
}
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
import { hasRequiredXCookies, loadXCookies } from "./cookies.js";
import { fetchTweetThread } from "./thread.js";
import { formatArticleMarkdown } from "./markdown.js";
import { resolveReferencedTweetsFromArticle } from "./referenced-tweets.js";
import { formatThreadTweetsMarkdown } from "./thread-markdown.js";
import { resolveArticleEntityFromTweet } from "./tweet-article.js";
@@ -123,22 +124,16 @@ export async function tweetToMarkdown(
const requestedUrl = normalizedUrl || buildTweetUrl(username, tweetId) || inputUrl.trim();
const rootUrl = buildTweetUrl(username, thread.rootId ?? tweetId) ?? requestedUrl;
const meta = formatMetaMarkdown({
url: rootUrl,
requested_url: requestedUrl,
author,
author_name: name ?? null,
author_username: username ?? null,
author_url: authorUrl ?? null,
tweet_count: thread.totalTweets ?? tweets.length,
});
const parts: string[] = [meta];
const articleEntity = await resolveArticleEntityFromTweet(firstTweet, cookieMap);
let coverImage: string | null = null;
let remainingTweets = tweets;
const parts: string[] = [];
if (articleEntity) {
const articleMarkdown = formatArticleMarkdown(articleEntity).trimEnd();
const referencedTweets = await resolveReferencedTweetsFromArticle(articleEntity, cookieMap, { log });
const articleResult = formatArticleMarkdown(articleEntity, { referencedTweets });
coverImage = articleResult.coverUrl;
const articleMarkdown = articleResult.markdown.trimEnd();
if (articleMarkdown) {
parts.push(articleMarkdown);
const firstTweetText = extractTweetText(firstTweet);
@@ -148,6 +143,19 @@ export async function tweetToMarkdown(
}
}
const meta = formatMetaMarkdown({
url: rootUrl,
requestedUrl: requestedUrl,
author,
authorName: name ?? null,
authorUsername: username ?? null,
authorUrl: authorUrl ?? null,
tweetCount: thread.totalTweets ?? tweets.length,
coverImage,
});
parts.unshift(meta);
if (remainingTweets.length > 0) {
const hasArticle = parts.length > 1;
if (hasArticle) {
@@ -40,6 +40,7 @@ export type ArticleEntityMapEntry = {
caption?: string;
mediaItems?: ArticleEntityMapMediaItem[];
url?: string;
tweetId?: string;
};
};
};
+187 -145
View File
@@ -5,31 +5,36 @@ description: Formats plain text or markdown files with frontmatter, titles, summ
# Markdown Formatter
Transforms plain text or markdown files into well-structured markdown with proper frontmatter, formatting, and typography.
Transforms plain text or markdown into well-structured, reader-friendly markdown. The goal is to help readers quickly grasp key points, highlights, and structure — without changing any original content.
**Core principle**: Only adjust formatting and fix obvious typos. Never add, delete, or rewrite content.
## 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 |
|--------|---------|
| `scripts/main.ts` | Main entry point with CLI options |
| `scripts/cjk-emphasis.ts` | Fix CJK emphasis/bold punctuation issues |
| `scripts/main.ts` | Main entry point with CLI options (uses remark-cjk-friendly for CJK emphasis) |
| `scripts/quotes.ts` | Replace ASCII quotes with fullwidth quotes |
| `scripts/autocorrect.ts` | Add CJK/English spacing via autocorrect |
## Preferences (EXTEND.md)
Use Bash to check EXTEND.md existence (priority order):
Check EXTEND.md existence (priority order):
```bash
# Check project-level first
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-format-markdown/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-format-markdown/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-format-markdown/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-format-markdown/EXTEND.md") { "user" }
```
┌──────────────────────────────────────────────────────────┬───────────────────┐
│ Path │ Location │
├──────────────────────────────────────────────────────────┼───────────────────┤
@@ -46,55 +51,45 @@ test -f "$HOME/.baoyu-skills/baoyu-format-markdown/EXTEND.md" && echo "user"
│ Not found │ Use defaults │
└───────────┴───────────────────────────────────────────────────────────────────────────┘
**EXTEND.md Supports**: Default formatting options | Summary length preferences
**EXTEND.md Supports**:
| Setting | Values | Default | Description |
|---------|--------|---------|-------------|
| `auto_select` | `true`/`false` | `false` | Skip both title and summary selection, auto-pick best |
| `auto_select_title` | `true`/`false` | `false` | Skip title selection only |
| `auto_select_summary` | `true`/`false` | `false` | Skip summary selection only |
| Other | — | — | Default formatting options, typography preferences |
## Usage
Claude performs content analysis and formatting (Steps 1-6), then runs the script for typography fixes (Step 7).
The workflow has two phases: **Analyze** (understand the content) then **Format** (apply formatting). Claude performs content analysis and formatting (Steps 1-5), then runs the script for typography fixes (Step 6).
## Workflow
### Step 1: Read Source File
### Step 1: Read & Detect Content Type
Read the user-specified markdown or plain text file.
### Step 1.5: Detect Content Type & Confirm
**Content Type Detection:**
Read the user-specified file, then detect content type:
| Indicator | Classification |
|-----------|----------------|
| Has `---` YAML frontmatter | Markdown |
| Has `#`, `##`, `###` headings | Markdown |
| Has `**bold**`, `*italic*` | Markdown |
| Has `- ` or `1. ` lists | Markdown |
| Has ``` code blocks | Markdown |
| Has `> ` blockquotes | Markdown |
| Has `**bold**`, `*italic*`, lists, code blocks, blockquotes | Markdown |
| None of above | Plain text |
**Decision Flow:**
┌─────────────────┬────────────────────────────────────────────────┐
│ Content Type │ Action │
├─────────────────┼────────────────────────────────────────────────┤
│ Plain text │ Proceed to Step 2 (format to markdown) │
├─────────────────┼────────────────────────────────────────────────┤
│ Markdown │ Use AskUserQuestion to confirm optimization │
└─────────────────┴────────────────────────────────────────────────┘
**If Markdown detected, ask user:**
```
Detected existing markdown formatting. What would you like to do?
1. Optimize formatting (Recommended)
- Add/improve frontmatter, headings, bold, lists
- Analyze content, improve headings, bold, lists for readability
- Run typography script (spacing, emphasis fixes)
- Output: {filename}-formatted.md
2. Keep original formatting
- Preserve existing markdown structure
- Run typography script (spacing, emphasis fixes)
- Run typography script only
- Output: {filename}-formatted.md
3. Typography fixes only
@@ -103,115 +98,171 @@ Detected existing markdown formatting. What would you like to do?
```
**Based on user choice:**
- **Optimize**: Continue to Step 2-8 (full workflow)
- **Keep original**: Skip Steps 2-5, copy file → Step 6-8 (run script on copy)
- **Typography only**: Skip Steps 2-6, run Step 7 on original file directly
- **Optimize**: Continue to Step 2 (full workflow)
- **Keep original**: Skip to Step 5, copy file then run Step 6
- **Typography only**: Skip to Step 6, run on original file directly
### Step 2: Analyze Content Structure
### Step 2: Analyze Content (Reader's Perspective)
Identify:
- Existing title (H1 `#`)
- Paragraph separations
- Keywords suitable for **bold**
- Parallel content convertible to lists
- Code snippets
- Quotations
Read the entire content carefully. Think from a reader's perspective: what would help them quickly understand and remember the key information?
### Step 3: Check/Create Frontmatter
Produce an analysis covering these dimensions:
**2.1 Highlights & Key Insights**
- Core arguments or conclusions the author makes
- Surprising facts, data points, or counterintuitive claims
- Memorable quotes or well-phrased sentences (golden quotes)
**2.2 Structure Assessment**
- Does the content have a clear logical flow? What is it?
- Are there natural section boundaries that lack headings?
- Are there long walls of text that could benefit from visual breaks?
**2.3 Reader-Important Information**
- Actionable advice or takeaways
- Definitions, explanations of key concepts
- Lists or enumerations buried in prose
- Comparisons or contrasts that would be clearer as tables
**2.4 Formatting Issues**
- Missing or inconsistent heading hierarchy
- Paragraphs that mix multiple topics
- Parallel items written as prose instead of lists
- Code, commands, or technical terms not marked as code
- Obvious typos or formatting errors
**Save analysis to file**: `{original-filename}-analysis.md`
The analysis file serves as the blueprint for Step 3. Use this format:
```markdown
# Content Analysis: {filename}
## Highlights & Key Insights
- [list findings]
## Structure Assessment
- Current flow: [describe]
- Suggested sections: [list heading candidates with brief rationale]
## Reader-Important Information
- [list actionable items, key concepts, buried lists, potential tables]
## Formatting Issues
- [list specific issues with location references]
## Typos Found
- [list any obvious typos with corrections, or "None found"]
```
### Step 3: Check/Create Frontmatter, Title & Summary
Check for YAML frontmatter (`---` block). Create if missing.
**Meta field handling:**
| Field | Processing |
|-------|------------|
| `title` | See Step 4 |
| `slug` | Infer from file path (e.g., `posts/2026/01/10/vibe-coding/``vibe-coding`) or generate from title |
| `summary` | Generate engaging summary (100-150 characters) |
| `featureImage` | Check if `imgs/cover.png` exists in same directory; if so, use relative path |
| `title` | See **Title Generation** below |
| `slug` | Infer from file path or generate from title |
| `summary` | See **Summary Generation** below |
| `coverImage` | Check if `imgs/cover.png` exists in same directory; if so, use relative path |
### Step 4: Title Handling
**Title Generation:**
**Logic:**
1. If frontmatter already has `title` → use it, no H1 in body
2. If first line is H1 → extract to frontmatter `title`, remove H1 from body
3. If neither exists → generate candidate titles
**Title generation flow:**
1. Generate 3 candidate titles based on content
2. Use `AskUserQuestion` tool:
Generate 3 candidate titles with different angles/styles. Present to user for selection:
```
Select a title:
Pick a title:
1. [Title A] (Recommended)
2. [Title B]
3. [Title C]
1. [Title A] — [angle/style note]
2. [Title B] — [angle/style note]
3. [Title C] — [angle/style note]
Enter number, or type a custom title:
```
3. If no selection within a few seconds, use recommended (option 1)
**Title principles:**
- Concise, max 20 characters
- Captures core message
Title principles:
- Engaging, sparks reading interest
- Captures core message or most compelling angle
- Accurate, avoids clickbait
- Vary angles: e.g. story-driven, conclusion-driven, question-driven
**Important:** Once title is in frontmatter, body should NOT have H1 (avoid duplication)
If frontmatter already has `title`, skip selection and use it. If first line is H1, extract to frontmatter as default title but still offer alternatives.
### Step 5: Format Processing
**Summary Generation:**
**Formatting rules:**
Generate 3 candidate summaries with different focuses. Present to user for selection:
| Element | Format |
|---------|--------|
| Titles | Use `#`, `##`, `###` hierarchy |
| Key points | Use `**bold**` |
| Parallel items | Convert to `-` unordered or `1.` ordered lists |
| Code/commands | Use `` `inline` `` or ` ```block``` ` |
| Quotes/sayings | Use `>` blockquote |
| Separators | Use `---` where appropriate |
```
Pick a summary:
**Formatting principles:**
- Preserve original content and viewpoints
- Add formatting only, do not modify text
- Formatting serves readability
- Avoid over-formatting
1. [Summary A] — [focus note]
2. [Summary B] — [focus note]
3. [Summary C] — [focus note]
### Step 6: Save Formatted File
Enter number, or type a custom summary:
```
Summary principles:
- 80-150 characters, precise and information-rich
- Convey article's core value, not just topic
- Vary focuses: e.g. problem-driven, result-driven, insight-driven
- Avoid generic descriptions like "本文介绍了..."
If frontmatter already has `summary`, skip selection and use it.
**EXTEND.md skip behavior:** If `auto_select: true` is set in EXTEND.md, skip title and summary selection — generate the best candidate directly without asking. User can also set `auto_select_title: true` or `auto_select_summary: true` independently.
Once title is in frontmatter, body should NOT have H1 (avoid duplication).
### Step 4: Format Content
Apply formatting guided by the Step 2 analysis. The goal is making the content scannable and the key points impossible to miss.
**Formatting toolkit:**
| Element | When to use | Format |
|---------|-------------|--------|
| Headings | Natural topic boundaries, section breaks | `##`, `###` hierarchy |
| Bold | Key conclusions, important terms, core takeaways | `**bold**` |
| Unordered lists | Parallel items, feature lists, examples | `- item` |
| Ordered lists | Sequential steps, ranked items, procedures | `1. item` |
| Tables | Comparisons, structured data, option matrices | Markdown table |
| Code | Commands, file paths, technical terms, variable names | `` `inline` `` or fenced blocks |
| Blockquotes | Notable quotes, important warnings, cited text | `> quote` |
| Separators | Major topic transitions | `---` |
**Formatting principles — what NOT to do:**
- Do NOT add sentences, explanations, or commentary
- Do NOT delete or shorten any content
- Do NOT rephrase or rewrite the author's words
- Do NOT add headings that editorialize (e.g., "Amazing Discovery" — use neutral descriptive headings)
- Do NOT over-format: not every sentence needs bold, not every paragraph needs a heading
**Formatting principles — what TO do:**
- Preserve the author's voice, tone, and every word
- Use bold sparingly for genuinely important points
- Extract parallel items from prose into lists only when the structure is clearly there
- Add headings where the topic genuinely shifts
- Fix obvious typos (based on Step 2 findings)
### Step 5: Save Formatted File
Save as `{original-filename}-formatted.md`
Examples:
- `final.md``final-formatted.md`
- `draft-v1.md``draft-v1-formatted.md`
**If user chose "Keep original formatting" (from Step 1.5):**
- Copy original file to `{filename}-formatted.md` without modifications
- Proceed to Step 7 for typography fixes only
**Backup existing file:**
If `{filename}-formatted.md` already exists, backup before overwriting:
```bash
# Check if formatted file exists
if [ -f "{filename}-formatted.md" ]; then
# Backup with timestamp
mv "{filename}-formatted.md" "{filename}-formatted.backup-$(date +%Y%m%d-%H%M%S).md"
fi
```
Example:
- `final-formatted.md` exists → backup to `final-formatted.backup-20260128-143052.md`
### Step 6: Execute Typography Script
### Step 7: Execute Text Formatting Script
After saving, **must** run the formatting script:
Run the formatting script on the output file:
```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:**
@@ -224,75 +275,66 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts {output-file-path} [options]
| `--no-spacing` | | Do not add CJK/English spacing | |
| `--emphasis` | `-e` | Fix CJK emphasis punctuation issues | true |
| `--no-emphasis` | | Do not fix CJK emphasis issues | |
| `--help` | `-h` | Show help message | |
**Examples:**
```bash
# 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
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
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --no-spacing
# 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
```
**Script performs (based on options):**
1. Fix CJK emphasis/bold punctuation issues (default: enabled)
2. Add CJK/English mixed text spacing via autocorrect (default: enabled)
3. Replace ASCII quotes `"..."` with fullwidth quotes `"..."` (default: disabled)
3. Replace ASCII quotes with fullwidth quotes (default: disabled)
4. Format frontmatter YAML (always enabled)
### Step 8: Display Results
### Step 7: Completion Report
Display a report summarizing all changes made:
```
**Formatting complete**
**Formatting Complete**
File: posts/2026/01/09/example/final-formatted.md
**Files:**
- Analysis: {filename}-analysis.md
- Formatted: {filename}-formatted.md
Changes:
- Added title: [title content]
- Added X bold markers
- Added X lists
- Added X code blocks
**Content Analysis Summary:**
- Highlights found: X key insights
- Golden quotes: X memorable sentences
- Formatting issues fixed: X items
**Changes Applied:**
- Frontmatter: [added/updated] (title, slug, summary)
- Headings added: X (##: N, ###: N)
- Bold markers added: X
- Lists created: X (from prose → list conversion)
- Tables created: X
- Code markers added: X
- Blockquotes added: X
- Typos fixed: X [list each: "原文" → "修正"]
**Typography Script:**
- CJK spacing: [applied/skipped]
- Emphasis fixes: [applied/skipped]
- Quote replacement: [applied/skipped]
```
## Formatting Example
**Before:**
```
This is plain text. First point is efficiency improvement. Second point is cost reduction. Third point is experience optimization. Use npm install to install dependencies.
```
**After:**
```markdown
---
title: Three Core Advantages
slug: three-core-advantages
summary: Discover the three key benefits that drive success in modern projects.
---
This is plain text.
**Main advantages:**
- Efficiency improvement
- Cost reduction
- Experience optimization
Use `npm install` to install dependencies.
```
Adjust the report to reflect actual changes — omit categories where no changes were made.
## Notes
- Preserve original writing style and tone
- Specify correct language for code blocks (e.g., `python`, `javascript`)
- Maintain CJK/English spacing standards
- Do not add content not present in original
- The analysis file is a working document — it helps maintain consistency between what was identified and what was formatted
## Extension Support
@@ -1,314 +0,0 @@
const CJK_PUNCT_COMMON = "。.,、?!:;";
const CJK_OPENING_PUNCT = "(〔〖〘〚「『〈《【\u201C\u2018";
const CJK_CLOSING_PUNCT = ")〕〗〙〛」』〉》】\u201D\u2019" + CJK_PUNCT_COMMON;
const CJK_SCRIPTS =
"\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}";
export const CJK_CLOSING_PUNCT_RE = new RegExp(`[${CJK_CLOSING_PUNCT}]`);
export const CJK_OPENING_PUNCT_RE = new RegExp(`^[${CJK_OPENING_PUNCT}]`);
export const CJK_CHAR_RE = new RegExp(`[${CJK_SCRIPTS}]`, "u");
const PUNCT_OR_SYMBOL_RE = /[\p{P}\p{S}]/u;
const WORD_CHAR_RE = /[\p{L}\p{N}]/u;
const CJK_PUNCT_PAIRS: Record<string, string> = {
"“": "”",
"": "",
"": "",
"": "",
"〖": "〗",
"〘": "〙",
"〚": "〛",
"「": "」",
"『": "』",
"〈": "〉",
"《": "》",
"【": "】",
};
function findInlineCodeRanges(text: string): Array<[number, number]> {
const ranges: Array<[number, number]> = [];
let i = 0;
while (i < text.length) {
if (text[i] !== "`") {
i += 1;
continue;
}
let run = 1;
while (i + run < text.length && text[i + run] === "`") {
run += 1;
}
const start = i;
let j = i + run;
let found = false;
while (j < text.length) {
if (text[j] !== "`") {
j += 1;
continue;
}
let closeRun = 1;
while (j + closeRun < text.length && text[j + closeRun] === "`") {
closeRun += 1;
}
if (closeRun === run) {
ranges.push([start, j + closeRun - 1]);
i = j + closeRun;
found = true;
break;
}
j += closeRun;
}
if (!found) {
i = start + run;
}
}
return ranges;
}
function isEscaped(text: string, pos: number): boolean {
let count = 0;
for (let i = pos - 1; i >= 0 && text[i] === "\\"; i -= 1) {
count += 1;
}
return count % 2 === 1;
}
function isWhitespaceChar(ch: string | undefined): boolean {
return !ch || /\s/u.test(ch);
}
function isPunctuationOrSymbol(ch: string | undefined): boolean {
return !!ch && PUNCT_OR_SYMBOL_RE.test(ch);
}
function isMatchingCjkPunct(open: string, close: string): boolean {
return CJK_PUNCT_PAIRS[open] === close;
}
function mapNonCodeSegments(
block: string,
mapper: (segment: string) => string
): string {
const codeRanges = findInlineCodeRanges(block);
if (codeRanges.length === 0) {
return mapper(block);
}
let result = "";
let lastIndex = 0;
for (const [start, end] of codeRanges) {
if (start > lastIndex) {
result += mapper(block.slice(lastIndex, start));
}
result += block.slice(start, end + 1);
lastIndex = end + 1;
}
if (lastIndex < block.length) {
result += mapper(block.slice(lastIndex));
}
return result;
}
function moveCjkPunctuationOutsideEmphasis(block: string): string {
return mapNonCodeSegments(block, (segment) => {
const delimiterPositions: number[] = [];
let cursor = 0;
while (cursor < segment.length - 1) {
if (
segment[cursor] === "*" &&
segment[cursor + 1] === "*" &&
segment[cursor - 1] !== "*" &&
segment[cursor + 2] !== "*" &&
!isEscaped(segment, cursor)
) {
delimiterPositions.push(cursor);
cursor += 2;
continue;
}
cursor += 1;
}
if (delimiterPositions.length < 2) return segment;
const stack: number[] = [];
const pairs: Array<{ open: number; close: number }> = [];
for (const pos of delimiterPositions) {
if (stack.length === 0) {
stack.push(pos);
} else {
const open = stack.pop() as number;
pairs.push({ open, close: pos });
}
}
const skip = new Set<number>();
const insertBefore = new Map<number, string>();
for (const pair of pairs) {
const openPunctPos = pair.open + 2;
const closePunctPos = pair.close - 1;
if (openPunctPos >= closePunctPos) continue;
const openPunct = segment[openPunctPos];
const closePunct = segment[closePunctPos];
if (!openPunct || !closePunct) continue;
if (!CJK_OPENING_PUNCT_RE.test(openPunct)) continue;
if (!CJK_CLOSING_PUNCT_RE.test(closePunct)) continue;
if (!isMatchingCjkPunct(openPunct, closePunct)) continue;
if (openPunctPos + 1 >= closePunctPos) continue;
const inner = segment.slice(openPunctPos + 1, closePunctPos);
if (inner.length === 0) continue;
skip.add(openPunctPos);
skip.add(closePunctPos);
insertBefore.set(pair.open, (insertBefore.get(pair.open) ?? "") + openPunct);
const afterClose = pair.close + 2;
insertBefore.set(
afterClose,
(insertBefore.get(afterClose) ?? "") + closePunct
);
}
if (skip.size === 0) return segment;
let result = "";
for (let idx = 0; idx < segment.length; idx += 1) {
const insert = insertBefore.get(idx);
if (insert) {
result += insert;
}
if (skip.has(idx)) {
continue;
}
result += segment[idx];
}
const tailInsert = insertBefore.get(segment.length);
if (tailInsert) {
result += tailInsert;
}
return result;
});
}
function fixCjkEmphasisSpacingInBlock(block: string): string {
const normalized = moveCjkPunctuationOutsideEmphasis(block);
const codeRanges = findInlineCodeRanges(normalized);
let rangeIndex = 0;
const delimiters: Array<{
pos: number;
canOpen: boolean;
canClose: boolean;
}> = [];
let cursor = 0;
while (cursor < normalized.length - 1) {
if (rangeIndex < codeRanges.length && cursor >= codeRanges[rangeIndex][0]) {
if (cursor <= codeRanges[rangeIndex][1]) {
cursor = codeRanges[rangeIndex][1] + 1;
continue;
}
rangeIndex += 1;
continue;
}
if (
normalized[cursor] === "*" &&
normalized[cursor + 1] === "*" &&
normalized[cursor - 1] !== "*" &&
normalized[cursor + 2] !== "*" &&
!isEscaped(normalized, cursor)
) {
const before = normalized[cursor - 1];
const after = normalized[cursor + 2];
const beforeIsSpace = isWhitespaceChar(before);
const afterIsSpace = isWhitespaceChar(after);
const beforeIsPunct = isPunctuationOrSymbol(before);
const afterIsPunct = isPunctuationOrSymbol(after);
const leftFlanking =
!afterIsSpace && (!afterIsPunct || beforeIsSpace || beforeIsPunct);
const rightFlanking =
!beforeIsSpace && (!beforeIsPunct || afterIsSpace || afterIsPunct);
const cjkPunctBefore = !!before && CJK_CLOSING_PUNCT_RE.test(before);
const wordAfter = !!after && WORD_CHAR_RE.test(after);
delimiters.push({
pos: cursor,
canOpen: leftFlanking,
canClose: rightFlanking || (cjkPunctBefore && wordAfter),
});
cursor += 2;
continue;
}
cursor += 1;
}
const stack: Array<{ pos: number }> = [];
const pairs: Array<{ open: number; close: number }> = [];
for (const delimiter of delimiters) {
if (delimiter.canClose) {
let openerIndex = -1;
for (let j = stack.length - 1; j >= 0; j -= 1) {
openerIndex = j;
break;
}
if (openerIndex !== -1) {
const opener = stack.splice(openerIndex, 1)[0];
pairs.push({ open: opener.pos, close: delimiter.pos });
}
}
if (delimiter.canOpen) {
stack.push({ pos: delimiter.pos });
}
}
if (pairs.length === 0) return normalized;
const insertPositions = new Set<number>();
for (const pair of pairs) {
const insideLast = normalized[pair.close - 1];
const afterClose = normalized[pair.close + 2];
if (!afterClose) continue;
if (
CJK_CLOSING_PUNCT_RE.test(insideLast) &&
WORD_CHAR_RE.test(afterClose)
) {
insertPositions.add(pair.close + 2);
}
}
if (insertPositions.size === 0) return normalized;
let result = "";
for (let idx = 0; idx < normalized.length; idx += 1) {
if (insertPositions.has(idx)) {
result += " ";
}
result += normalized[idx];
}
if (insertPositions.has(normalized.length)) {
result += " ";
}
return result;
}
export function fixCjkEmphasisSpacing(content: string): string {
const parts = content.split(/(^```[\s\S]*?^```|^~~~[\s\S]*?^~~~)/m);
return parts
.map((part, i) => {
if (i % 2 === 1) return part;
const blocks = part.split(/(\n\s*\n+)/);
return blocks
.map((block, index) => {
if (index % 2 === 1) return block;
return fixCjkEmphasisSpacingInBlock(block);
})
.join("");
})
.join("");
}
+10 -55
View File
@@ -1,17 +1,12 @@
import { readFileSync, writeFileSync } from "fs";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkCjkFriendly from "remark-cjk-friendly";
import remarkGfm from "remark-gfm";
import remarkFrontmatter from "remark-frontmatter";
import remarkStringify from "remark-stringify";
import { visit } from "unist-util-visit";
import YAML from "yaml";
import {
fixCjkEmphasisSpacing,
CJK_CLOSING_PUNCT_RE,
CJK_OPENING_PUNCT_RE,
CJK_CHAR_RE,
} from "./cjk-emphasis";
import { replaceQuotes } from "./quotes";
import { applyAutocorrect } from "./autocorrect";
@@ -36,6 +31,12 @@ const DEFAULT_OPTIONS: Required<FormatOptions> = {
emphasis: true,
};
function decodeHtmlEntities(text: string): string {
return text.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) =>
String.fromCodePoint(parseInt(hex, 16))
);
}
function formatFrontmatter(value: string): string | null {
try {
const doc = YAML.parseDocument(value);
@@ -49,12 +50,9 @@ function formatMarkdownContent(
content: string,
options: Required<FormatOptions>
): string {
if (options.emphasis) {
content = fixCjkEmphasisSpacing(content);
}
const processor = unified()
.use(remarkParse)
.use(options.emphasis ? remarkCjkFriendly : [])
.use(remarkGfm)
.use(remarkFrontmatter, ["yaml"])
.use(remarkStringify, {
@@ -63,7 +61,7 @@ function formatMarkdownContent(
const tree = processor.parse(content);
visit(tree, (node, _index, parent) => {
visit(tree, (node) => {
if (node.type === "text" && options.quotes) {
const textNode = node as { value: string };
textNode.value = replaceQuotes(textNode.value);
@@ -77,54 +75,11 @@ function formatMarkdownContent(
}
return;
}
if (
options.emphasis &&
(node.type === "strong" ||
node.type === "emphasis" ||
node.type === "delete") &&
parent
) {
const siblings = (parent as { children: typeof node[] }).children;
const idx = siblings.indexOf(node);
const children = (node as { children: typeof node[] }).children;
if (!children || children.length === 0) return;
const lastChild = children[children.length - 1];
if (lastChild.type === "text") {
const lastText = (lastChild as { value: string }).value;
if (
CJK_CLOSING_PUNCT_RE.test(lastText.slice(-1)) &&
idx + 1 < siblings.length
) {
const nextSib = siblings[idx + 1];
if (nextSib.type === "text") {
const nextText = (nextSib as { value: string }).value;
if (CJK_CHAR_RE.test(nextText.charAt(0))) {
(nextSib as { value: string }).value = " " + nextText;
}
}
}
}
const firstChild = children[0];
if (firstChild.type === "text") {
const firstText = (firstChild as { value: string }).value;
if (CJK_OPENING_PUNCT_RE.test(firstText) && idx > 0) {
const prevSib = siblings[idx - 1];
if (prevSib.type === "text") {
const prevText = (prevSib as { value: string }).value;
if (CJK_CHAR_RE.test(prevText.charAt(prevText.length - 1))) {
(prevSib as { value: string }).value = prevText + " ";
}
}
}
}
}
});
let result = processor.stringify(tree);
if (options.emphasis) {
result = fixCjkEmphasisSpacing(result);
result = decodeHtmlEntities(result);
}
return result;
}
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
{
"dependencies": {
"remark-cjk-friendly": "^1.1.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
+88 -38
View File
@@ -1,71 +1,85 @@
---
name: baoyu-image-gen
description: AI image generation with OpenAI, Google and DashScope APIs. Supports text-to-image, reference images, aspect ratios. Sequential by default; parallel generation available on request. Use when user asks to generate, create, or draw images.
description: AI image generation with OpenAI, Google, DashScope and Replicate APIs. Supports text-to-image, reference images, aspect ratios. Sequential by default; parallel generation available on request. Use when user asks to generate, create, or draw images.
---
# Image Generation (AI SDK)
Official API-based image generation. Supports OpenAI, Google and DashScope (阿里通义万象) providers.
Official API-based image generation. Supports OpenAI, Google, DashScope (阿里通义万象) and Replicate providers.
## Script Directory
**Agent Execution**:
1. `SKILL_DIR` = this SKILL.md file's directory
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
## Preferences (EXTEND.md)
## Step 0: Load Preferences ⛔ BLOCKING
Use Bash to check EXTEND.md existence (priority order):
**CRITICAL**: This step MUST complete BEFORE any image generation. Do NOT skip or defer.
Check EXTEND.md existence (priority: project → user):
```bash
# Check project-level first
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-image-gen/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md" && echo "user"
```
┌──────────────────────────────────────────────────┬───────────────────┐
│ Path │ Location │
├──────────────────────────────────────────────────┼───────────────────┤
.baoyu-skills/baoyu-image-gen/EXTEND.md │ Project directory │
├──────────────────────────────────────────────────┼───────────────────┤
│ $HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md │ User home │
└──────────────────────────────────────────────────┴───────────────────┘
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-image-gen/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md") { "user" }
```
┌───────────┬───────────────────────────────────────────────────────────────────────────┐
│ Result │ Action │
├───────────┼───────────────────────────────────────────────────────────────────────────┤
│ Found │ Read, parse, apply settings │
├───────────┼───────────────────────────────────────────────────────────────────────────┤
│ Not found │ Use defaults │
└───────────┴───────────────────────────────────────────────────────────────────────────┘
| Result | Action |
|--------|--------|
| Found | Load, parse, apply settings. If `default_model.[provider]` is null → ask model only (Flow 2) |
| Not found | ⛔ Run first-time setup ([references/config/first-time-setup.md](references/config/first-time-setup.md)) → Save EXTEND.md → Then continue |
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio
**CRITICAL**: If not found, complete the full setup (provider + model + quality + save location) using AskUserQuestion BEFORE generating any images. Generation is BLOCKED until EXTEND.md is created.
| Path | Location |
|------|----------|
| `.baoyu-skills/baoyu-image-gen/EXTEND.md` | Project directory |
| `$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md` | User home |
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio | Default image size | Default models
Schema: `references/config/preferences-schema.md`
## Usage
```bash
# 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
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
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
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 only)
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
# With reference images (Google multimodal or OpenAI edits)
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
# With reference images (explicit provider/model)
${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
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 (阿里通义万象)
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)
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
# Replicate with specific model
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
```
## Options
@@ -75,13 +89,13 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "一只可爱的猫" --image ou
| `--prompt <text>`, `-p` | Prompt text |
| `--promptfiles <files...>` | Read prompt from files (concatenated) |
| `--image <path>` | Output image path (required) |
| `--provider google\|openai\|dashscope` | Force provider (default: google) |
| `--model <id>`, `-m` | Model ID |
| `--provider google\|openai\|dashscope\|replicate` | Force provider (default: google) |
| `--model <id>`, `-m` | Model ID (Google: `gemini-3-pro-image-preview`, `gemini-3.1-flash-image-preview`; OpenAI: `gpt-image-1.5`) |
| `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
| `--size <WxH>` | Size (e.g., `1024x1024`) |
| `--quality normal\|2k` | Quality preset (default: 2k) |
| `--imageSize 1K\|2K\|4K` | Image size for Google (default: from quality) |
| `--ref <files...>` | Reference images (Google multimodal only) |
| `--ref <files...>` | Reference images. Supported by Google multimodal (`gemini-3-pro-image-preview`, `gemini-3-flash-preview`, `gemini-3.1-flash-image-preview`) and OpenAI edits (GPT Image models). If provider omitted: Google first, then OpenAI |
| `--n <count>` | Number of images |
| `--json` | JSON output |
@@ -92,20 +106,56 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "一只可爱的猫" --image ou
| `OPENAI_API_KEY` | OpenAI API key |
| `GOOGLE_API_KEY` | Google API key |
| `DASHSCOPE_API_KEY` | DashScope API key (阿里云) |
| `REPLICATE_API_TOKEN` | Replicate API token |
| `OPENAI_IMAGE_MODEL` | OpenAI model override |
| `GOOGLE_IMAGE_MODEL` | Google model override |
| `DASHSCOPE_IMAGE_MODEL` | DashScope model override (default: z-image-turbo) |
| `REPLICATE_IMAGE_MODEL` | Replicate model override (default: google/nano-banana-pro) |
| `OPENAI_BASE_URL` | Custom OpenAI endpoint |
| `GOOGLE_BASE_URL` | Custom Google endpoint |
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint |
| `REPLICATE_BASE_URL` | Custom Replicate endpoint |
**Load Priority**: CLI args > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
**Load Priority**: CLI args > EXTEND.md > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
## Model Resolution
Model priority (highest → lowest), applies to all providers:
1. CLI flag: `--model <id>`
2. EXTEND.md: `default_model.[provider]`
3. Env var: `<PROVIDER>_IMAGE_MODEL` (e.g., `GOOGLE_IMAGE_MODEL`)
4. Built-in default
**EXTEND.md overrides env vars**. If both EXTEND.md `default_model.google: "gemini-3-pro-image-preview"` and env var `GOOGLE_IMAGE_MODEL=gemini-3.1-flash-image-preview` exist, EXTEND.md wins.
**Agent MUST display model info** before each generation:
- Show: `Using [provider] / [model]`
- Show switch hint: `Switch model: --model <id> | EXTEND.md default_model.[provider] | env <PROVIDER>_IMAGE_MODEL`
### Replicate Models
Supported model formats:
- `owner/name` (recommended for official models), e.g. `google/nano-banana-pro`
- `owner/name:version` (community models by version), e.g. `stability-ai/sdxl:<version>`
Examples:
```bash
# Use Replicate default model
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
# Override model explicitly
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
```
## Provider Selection
1. `--provider` specified → use it
2. Only one API key available → use that provider
3. Multiple available → default to Google
1. `--ref` provided + no `--provider` → auto-select Google first, then OpenAI, then Replicate
2. `--provider` specified → use it (if `--ref`, must be `google`, `openai`, or `replicate`)
3. Only one API key available → use that provider
4. Multiple available → default to Google
## Quality Presets
@@ -155,7 +205,7 @@ Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`
- Missing API key → error with setup instructions
- Generation failure → auto-retry once
- Invalid aspect ratio → warning, proceed with default
- Reference images with non-multimodal model → warning, ignore refs
- Reference images with unsupported provider/model → error with fix hint (switch to Google multimodal: `gemini-3-pro-image-preview`, `gemini-3.1-flash-image-preview`; or OpenAI GPT Image edits)
## Extension Support
@@ -0,0 +1,197 @@
---
name: first-time-setup
description: First-time setup and default model selection flow for baoyu-image-gen
---
# First-Time Setup
## Overview
Triggered when:
1. No EXTEND.md found → full setup (provider + model + preferences)
2. EXTEND.md found but `default_model.[provider]` is null → model selection only
## Setup Flow
```
No EXTEND.md found EXTEND.md found, model null
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ AskUserQuestion │ │ AskUserQuestion │
│ (full setup) │ │ (model only) │
└─────────────────────┘ └──────────────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ Create EXTEND.md │ │ Update EXTEND.md │
└─────────────────────┘ └──────────────────────┘
│ │
▼ ▼
Continue Continue
```
## Flow 1: No EXTEND.md (Full Setup)
**Language**: Use user's input language or saved language preference.
Use AskUserQuestion with ALL questions in ONE call:
### Question 1: Default Provider
```yaml
header: "Provider"
question: "Default image generation provider?"
options:
- label: "Google (Recommended)"
description: "Gemini multimodal - high quality, reference images, flexible sizes"
- label: "OpenAI"
description: "GPT Image - consistent quality, reliable output"
- label: "DashScope"
description: "Alibaba Cloud - z-image-turbo, good for Chinese content"
- label: "Replicate"
description: "Community models - nano-banana-pro, flexible model selection"
```
### Question 2: Default Google Model
Only show if user selected Google or auto-detect (no explicit provider).
```yaml
header: "Google Model"
question: "Default Google image generation model?"
options:
- label: "gemini-3-pro-image-preview (Recommended)"
description: "Highest quality, best for production use"
- label: "gemini-3.1-flash-image-preview"
description: "Fast generation, good quality, lower cost"
- label: "gemini-3-flash-preview"
description: "Fast generation, balanced quality and speed"
```
### Question 3: Default Quality
```yaml
header: "Quality"
question: "Default image quality?"
options:
- label: "2k (Recommended)"
description: "2048px - covers, illustrations, infographics"
- label: "normal"
description: "1024px - quick previews, drafts"
```
### Question 4: Save Location
```yaml
header: "Save"
question: "Where to save preferences?"
options:
- label: "Project (Recommended)"
description: ".baoyu-skills/ (this project only)"
- label: "User"
description: "~/.baoyu-skills/ (all projects)"
```
### Save Locations
| Choice | Path | Scope |
|--------|------|-------|
| Project | `.baoyu-skills/baoyu-image-gen/EXTEND.md` | Current project |
| User | `$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md` | All projects |
### EXTEND.md Template
```yaml
---
version: 1
default_provider: [selected provider or null]
default_quality: [selected quality]
default_aspect_ratio: null
default_image_size: null
default_model:
google: [selected google model or null]
openai: null
dashscope: null
replicate: null
---
```
## Flow 2: EXTEND.md Exists, Model Null
When EXTEND.md exists but `default_model.[current_provider]` is null, ask ONLY the model question for the current provider.
### Google Model Selection
```yaml
header: "Google Model"
question: "Choose a default Google image generation model?"
options:
- label: "gemini-3-pro-image-preview (Recommended)"
description: "Highest quality, best for production use"
- label: "gemini-3.1-flash-image-preview"
description: "Fast generation, good quality, lower cost"
- label: "gemini-3-flash-preview"
description: "Fast generation, balanced quality and speed"
```
### OpenAI Model Selection
```yaml
header: "OpenAI Model"
question: "Choose a default OpenAI image generation model?"
options:
- label: "gpt-image-1.5 (Recommended)"
description: "Latest GPT Image model, high quality"
- label: "gpt-image-1"
description: "Previous generation GPT Image model"
```
### DashScope Model Selection
```yaml
header: "DashScope Model"
question: "Choose a default DashScope image generation model?"
options:
- label: "z-image-turbo (Recommended)"
description: "Fast generation, good quality"
- label: "z-image-ultra"
description: "Higher quality, slower generation"
```
### Replicate Model Selection
```yaml
header: "Replicate Model"
question: "Choose a default Replicate image generation model?"
options:
- label: "google/nano-banana-pro (Recommended)"
description: "Google's fast image model on Replicate"
- label: "google/nano-banana"
description: "Google's base image model on Replicate"
```
### Update EXTEND.md
After user selects a model:
1. Read existing EXTEND.md
2. If `default_model:` section exists → update the provider-specific key
3. If `default_model:` section missing → add the full section:
```yaml
default_model:
google: [value or null]
openai: [value or null]
dashscope: [value or null]
replicate: [value or null]
```
Only set the selected provider's model; leave others as their current value or null.
## After Setup
1. Create directory if needed
2. Write/update EXTEND.md with frontmatter
3. Confirm: "Preferences saved to [path]"
4. Continue with image generation
@@ -0,0 +1,69 @@
---
name: preferences-schema
description: EXTEND.md YAML schema for baoyu-image-gen user preferences
---
# Preferences Schema
## Full Schema
```yaml
---
version: 1
default_provider: null # google|openai|dashscope|replicate|null (null = auto-detect)
default_quality: null # normal|2k|null (null = use default: 2k)
default_aspect_ratio: null # "16:9"|"1:1"|"4:3"|"3:4"|"2.35:1"|null
default_image_size: null # 1K|2K|4K|null (Google only, overrides quality)
default_model:
google: null # e.g., "gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview"
openai: null # e.g., "gpt-image-1.5"
dashscope: null # e.g., "z-image-turbo"
replicate: null # e.g., "google/nano-banana-pro"
---
```
## Field Reference
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `version` | int | 1 | Schema version |
| `default_provider` | string\|null | null | Default provider (null = auto-detect) |
| `default_quality` | string\|null | null | Default quality (null = 2k) |
| `default_aspect_ratio` | string\|null | null | Default aspect ratio |
| `default_image_size` | string\|null | null | Google image size (overrides quality) |
| `default_model.google` | string\|null | null | Google default model |
| `default_model.openai` | string\|null | null | OpenAI default model |
| `default_model.dashscope` | string\|null | null | DashScope default model |
| `default_model.replicate` | string\|null | null | Replicate default model |
## Examples
**Minimal**:
```yaml
---
version: 1
default_provider: google
default_quality: 2k
---
```
**Full**:
```yaml
---
version: 1
default_provider: google
default_quality: 2k
default_aspect_ratio: "16:9"
default_image_size: 2K
default_model:
google: "gemini-3-pro-image-preview"
openai: "gpt-image-1.5"
dashscope: "z-image-turbo"
replicate: "google/nano-banana-pro"
---
```
+162 -18
View File
@@ -1,8 +1,8 @@
import path from "node:path";
import process from "node:process";
import { homedir } from "node:os";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import type { CliArgs, Provider } from "./types";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import type { CliArgs, Provider, ExtendConfig } from "./types";
function printUsage(): void {
console.log(`Usage:
@@ -14,13 +14,13 @@ Options:
-p, --prompt <text> Prompt text
--promptfiles <files...> Read prompt from files (concatenated)
--image <path> Output image path (required)
--provider google|openai|dashscope Force provider (auto-detect by default)
--provider google|openai|dashscope|replicate Force provider (auto-detect by default)
-m, --model <id> Model ID
--ar <ratio> Aspect ratio (e.g., 16:9, 1:1, 4:3)
--size <WxH> Size (e.g., 1024x1024)
--quality normal|2k Quality preset (default: 2k)
--imageSize 1K|2K|4K Image size for Google (default: from quality)
--ref <files...> Reference images (Google multimodal only)
--ref <files...> Reference images (Google multimodal or OpenAI edits)
--n <count> Number of images (default: 1)
--json JSON output
-h, --help Show help
@@ -30,14 +30,18 @@ Environment variables:
GOOGLE_API_KEY Google API key
GEMINI_API_KEY Gemini API key (alias for GOOGLE_API_KEY)
DASHSCOPE_API_KEY DashScope API key ()
REPLICATE_API_TOKEN Replicate API token
OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-1.5)
GOOGLE_IMAGE_MODEL Default Google model (gemini-3-pro-image-preview)
DASHSCOPE_IMAGE_MODEL Default DashScope model (z-image-turbo)
REPLICATE_IMAGE_MODEL Default Replicate model (google/nano-banana-pro)
OPENAI_BASE_URL Custom OpenAI endpoint
OPENAI_IMAGE_USE_CHAT Use /chat/completions instead of /images/generations (true|false)
GOOGLE_BASE_URL Custom Google endpoint
DASHSCOPE_BASE_URL Custom DashScope endpoint
REPLICATE_BASE_URL Custom Replicate endpoint
Env file load order: CLI args > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
Env file load order: CLI args > EXTEND.md > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
}
function parseArgs(argv: string[]): CliArgs {
@@ -49,7 +53,7 @@ function parseArgs(argv: string[]): CliArgs {
model: null,
aspectRatio: null,
size: null,
quality: "2k",
quality: null,
imageSize: null,
referenceImages: [],
n: 1,
@@ -108,7 +112,7 @@ function parseArgs(argv: string[]): CliArgs {
if (a === "--provider") {
const v = argv[++i];
if (v !== "google" && v !== "openai" && v !== "dashscope") throw new Error(`Invalid provider: ${v}`);
if (v !== "google" && v !== "openai" && v !== "dashscope" && v !== "replicate") throw new Error(`Invalid provider: ${v}`);
out.provider = v;
continue;
}
@@ -215,6 +219,87 @@ async function loadEnv(): Promise<void> {
}
}
function extractYamlFrontMatter(content: string): string | null {
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*$/m);
return match ? match[1] : null;
}
function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
const config: Partial<ExtendConfig> = {};
const lines = yaml.split("\n");
let currentKey: string | null = null;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
if (trimmed.includes(":") && !trimmed.startsWith("-")) {
const colonIdx = trimmed.indexOf(":");
const key = trimmed.slice(0, colonIdx).trim();
let value = trimmed.slice(colonIdx + 1).trim();
if (value === "null" || value === "") {
value = "null";
}
if (key === "version") {
config.version = value === "null" ? 1 : parseInt(value, 10);
} else if (key === "default_provider") {
config.default_provider = value === "null" ? null : (value as Provider);
} else if (key === "default_quality") {
config.default_quality = value === "null" ? null : (value as "normal" | "2k");
} else if (key === "default_aspect_ratio") {
const cleaned = value.replace(/['"]/g, "");
config.default_aspect_ratio = cleaned === "null" ? null : cleaned;
} else if (key === "default_image_size") {
config.default_image_size = value === "null" ? null : (value as "1K" | "2K" | "4K");
} else if (key === "default_model") {
config.default_model = { google: null, openai: null, dashscope: null, replicate: null };
currentKey = "default_model";
} else if (currentKey === "default_model" && (key === "google" || key === "openai" || key === "dashscope" || key === "replicate")) {
const cleaned = value.replace(/['"]/g, "");
config.default_model![key] = cleaned === "null" ? null : cleaned;
}
}
}
return config;
}
async function loadExtendConfig(): Promise<Partial<ExtendConfig>> {
const home = homedir();
const cwd = process.cwd();
const paths = [
path.join(cwd, ".baoyu-skills", "baoyu-image-gen", "EXTEND.md"),
path.join(home, ".baoyu-skills", "baoyu-image-gen", "EXTEND.md"),
];
for (const p of paths) {
try {
const content = await readFile(p, "utf8");
const yaml = extractYamlFrontMatter(content);
if (!yaml) continue;
return parseSimpleYaml(yaml);
} catch {
continue;
}
}
return {};
}
function mergeConfig(args: CliArgs, extend: Partial<ExtendConfig>): CliArgs {
return {
...args,
provider: args.provider ?? extend.default_provider ?? null,
quality: args.quality ?? extend.default_quality ?? null,
aspectRatio: args.aspectRatio ?? extend.default_aspect_ratio ?? null,
imageSize: args.imageSize ?? extend.default_image_size ?? null,
};
}
async function readPromptFromFiles(files: string[]): Promise<string> {
const parts: string[] = [];
for (const f of files) {
@@ -242,28 +327,67 @@ function normalizeOutputImagePath(p: string): string {
}
function detectProvider(args: CliArgs): Provider {
if (args.referenceImages.length > 0 && args.provider && args.provider !== "google" && args.provider !== "openai" && args.provider !== "replicate") {
throw new Error(
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), or --provider replicate."
);
}
if (args.provider) return args.provider;
const hasGoogle = !!(process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY);
const hasOpenai = !!process.env.OPENAI_API_KEY;
const hasDashscope = !!process.env.DASHSCOPE_API_KEY;
const hasReplicate = !!process.env.REPLICATE_API_TOKEN;
const available = [hasGoogle && "google", hasOpenai && "openai", hasDashscope && "dashscope"].filter(Boolean) as Provider[];
if (args.referenceImages.length > 0) {
if (hasGoogle) return "google";
if (hasOpenai) return "openai";
if (hasReplicate) return "replicate";
throw new Error(
"Reference images require Google, OpenAI or Replicate. Set GOOGLE_API_KEY/GEMINI_API_KEY, OPENAI_API_KEY, or REPLICATE_API_TOKEN, or remove --ref."
);
}
const available = [hasGoogle && "google", hasOpenai && "openai", hasDashscope && "dashscope", hasReplicate && "replicate"].filter(Boolean) as Provider[];
if (available.length === 1) return available[0]!;
if (available.length > 1) return available[0]!;
throw new Error(
"No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, or DASHSCOPE_API_KEY.\n" +
"No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, DASHSCOPE_API_KEY, or REPLICATE_API_TOKEN.\n" +
"Create ~/.baoyu-skills/.env or <cwd>/.baoyu-skills/.env with your keys."
);
}
async function validateReferenceImages(referenceImages: string[]): Promise<void> {
for (const refPath of referenceImages) {
const fullPath = path.resolve(refPath);
try {
await access(fullPath);
} catch {
throw new Error(`Reference image not found: ${fullPath}`);
}
}
}
type ProviderModule = {
getDefaultModel: () => string;
generateImage: (prompt: string, model: string, args: CliArgs) => Promise<Uint8Array>;
};
function isRetryableGenerationError(error: unknown): boolean {
const msg = error instanceof Error ? error.message : String(error);
const nonRetryableMarkers = [
"Reference image",
"not supported",
"only supported",
"No API key found",
"is required",
];
return !nonRetryableMarkers.some((marker) => msg.includes(marker));
}
async function loadProviderModule(provider: Provider): Promise<ProviderModule> {
if (provider === "google") {
return (await import("./providers/google")) as ProviderModule;
@@ -271,6 +395,9 @@ async function loadProviderModule(provider: Provider): Promise<ProviderModule> {
if (provider === "dashscope") {
return (await import("./providers/dashscope")) as ProviderModule;
}
if (provider === "replicate") {
return (await import("./providers/replicate")) as ProviderModule;
}
return (await import("./providers/openai")) as ProviderModule;
}
@@ -283,9 +410,13 @@ async function main(): Promise<void> {
}
await loadEnv();
const extendConfig = await loadExtendConfig();
const mergedArgs = mergeConfig(args, extendConfig);
let prompt: string | null = args.prompt;
if (!prompt && args.promptFiles.length > 0) prompt = await readPromptFromFiles(args.promptFiles);
if (!mergedArgs.quality) mergedArgs.quality = "2k";
let prompt: string | null = mergedArgs.prompt;
if (!prompt && mergedArgs.promptFiles.length > 0) prompt = await readPromptFromFiles(mergedArgs.promptFiles);
if (!prompt) prompt = await readPromptFromStdin();
if (!prompt) {
@@ -295,27 +426,40 @@ async function main(): Promise<void> {
return;
}
if (!args.imagePath) {
if (!mergedArgs.imagePath) {
console.error("Error: --image is required");
printUsage();
process.exitCode = 1;
return;
}
const provider = detectProvider(args);
if (mergedArgs.referenceImages.length > 0) {
await validateReferenceImages(mergedArgs.referenceImages);
}
const provider = detectProvider(mergedArgs);
const providerModule = await loadProviderModule(provider);
const model = args.model || providerModule.getDefaultModel();
const outputPath = normalizeOutputImagePath(args.imagePath);
let model = mergedArgs.model;
if (!model && extendConfig.default_model) {
if (provider === "google") model = extendConfig.default_model.google ?? null;
if (provider === "openai") model = extendConfig.default_model.openai ?? null;
if (provider === "dashscope") model = extendConfig.default_model.dashscope ?? null;
if (provider === "replicate") model = extendConfig.default_model.replicate ?? null;
}
model = model || providerModule.getDefaultModel();
const outputPath = normalizeOutputImagePath(mergedArgs.imagePath);
let imageData: Uint8Array;
let retried = false;
while (true) {
try {
imageData = await providerModule.generateImage(prompt, model, args);
imageData = await providerModule.generateImage(prompt, model, mergedArgs);
break;
} catch (e) {
if (!retried) {
if (!retried && isRetryableGenerationError(e)) {
retried = true;
console.error("Generation failed, retrying...");
continue;
@@ -328,7 +472,7 @@ async function main(): Promise<void> {
await mkdir(dir, { recursive: true });
await writeFile(outputPath, imageData);
if (args.json) {
if (mergedArgs.json) {
console.log(
JSON.stringify(
{
@@ -22,27 +22,53 @@ function parseAspectRatio(ar: string): { width: number; height: number } | null
return { width: w, height: h };
}
function getSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
const baseSize = quality === "2k" ? 1440 : 1024;
const STANDARD_SIZES: [number, number][] = [
[1024, 1024],
[1280, 720],
[720, 1280],
[1024, 768],
[768, 1024],
[1536, 1024],
[1024, 1536],
[1536, 864],
[864, 1536],
];
if (!ar) return `${baseSize}*${baseSize}`;
const STANDARD_SIZES_2K: [number, number][] = [
[1536, 1536],
[2048, 1152],
[1152, 2048],
[1536, 1024],
[1024, 1536],
[1536, 864],
[864, 1536],
[2048, 2048],
];
function getSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
const is2k = quality === "2k";
const defaultSize = is2k ? "1536*1536" : "1024*1024";
if (!ar) return defaultSize;
const parsed = parseAspectRatio(ar);
if (!parsed) return `${baseSize}*${baseSize}`;
if (!parsed) return defaultSize;
const ratio = parsed.width / parsed.height;
const targetRatio = parsed.width / parsed.height;
const sizes = is2k ? STANDARD_SIZES_2K : STANDARD_SIZES;
if (Math.abs(ratio - 1) < 0.1) {
return `${baseSize}*${baseSize}`;
let best = defaultSize;
let bestDiff = Infinity;
for (const [w, h] of sizes) {
const diff = Math.abs(w / h - targetRatio);
if (diff < bestDiff) {
bestDiff = diff;
best = `${w}*${h}`;
}
}
if (ratio > 1) {
const w = Math.round(baseSize * ratio);
return `${w}*${baseSize}`;
}
const h = Math.round(baseSize / ratio);
return `${baseSize}*${h}`;
return best;
}
function normalizeSize(size: string): string {
@@ -58,7 +84,9 @@ export async function generateImage(
if (!apiKey) throw new Error("DASHSCOPE_API_KEY is required");
if (args.referenceImages.length > 0) {
console.error("Warning: Reference images not yet supported with DashScope, ignoring.");
throw new Error(
"Reference images are not supported with DashScope provider in baoyu-image-gen. Use --provider google with a Gemini multimodal model."
);
}
const size = args.size ? normalizeSize(args.size) : getSizeFromAspectRatio(args.aspectRatio, args.quality);
@@ -1,9 +1,17 @@
import path from "node:path";
import { readFile } from "node:fs/promises";
import { execSync } from "node:child_process";
import type { CliArgs } from "../types";
const GOOGLE_MULTIMODAL_MODELS = ["gemini-3-pro-image-preview", "gemini-3-flash-preview"];
const GOOGLE_IMAGEN_MODELS = ["imagen-3.0-generate-002", "imagen-3.0-generate-001"];
const GOOGLE_MULTIMODAL_MODELS = [
"gemini-3-pro-image-preview",
"gemini-3-flash-preview",
"gemini-3.1-flash-image-preview",
];
const GOOGLE_IMAGEN_MODELS = [
"imagen-3.0-generate-002",
"imagen-3.0-generate-001",
];
export function getDefaultModel(): string {
return process.env.GOOGLE_IMAGE_MODEL || "gemini-3-pro-image-preview";
@@ -33,7 +41,8 @@ function getGoogleImageSize(args: CliArgs): "1K" | "2K" | "4K" {
}
function getGoogleBaseUrl(): string {
const base = process.env.GOOGLE_BASE_URL || "https://generativelanguage.googleapis.com";
const base =
process.env.GOOGLE_BASE_URL || "https://generativelanguage.googleapis.com";
return base.replace(/\/+$/g, "");
}
@@ -49,11 +58,46 @@ function toModelPath(model: string): string {
return `models/${modelId}`;
}
async function postGoogleJson<T>(pathname: string, body: unknown): Promise<T> {
const apiKey = getGoogleApiKey();
if (!apiKey) throw new Error("GOOGLE_API_KEY or GEMINI_API_KEY is required");
function getHttpProxy(): string | null {
return (
process.env.https_proxy ||
process.env.HTTPS_PROXY ||
process.env.http_proxy ||
process.env.HTTP_PROXY ||
process.env.ALL_PROXY ||
null
);
}
const res = await fetch(buildGoogleUrl(pathname), {
async function postGoogleJsonViaCurl<T>(
url: string,
apiKey: string,
body: unknown,
): Promise<T> {
const proxy = getHttpProxy();
const bodyStr = JSON.stringify(body);
const proxyArgs = proxy ? `-x "${proxy}"` : "";
const result = execSync(
`curl -s --connect-timeout 30 --max-time 300 ${proxyArgs} "${url}" -H "Content-Type: application/json" -H "x-goog-api-key: ${apiKey}" -d @-`,
{ input: bodyStr, maxBuffer: 100 * 1024 * 1024, timeout: 310000 },
);
const parsed = JSON.parse(result.toString()) as any;
if (parsed.error) {
throw new Error(
`Google API error (${parsed.error.code}): ${parsed.error.message}`,
);
}
return parsed as T;
}
async function postGoogleJsonViaFetch<T>(
url: string,
apiKey: string,
body: unknown,
): Promise<T> {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -70,7 +114,30 @@ async function postGoogleJson<T>(pathname: string, body: unknown): Promise<T> {
return (await res.json()) as T;
}
function buildPromptWithAspect(prompt: string, ar: string | null, quality: CliArgs["quality"]): string {
async function postGoogleJson<T>(pathname: string, body: unknown): Promise<T> {
const apiKey = getGoogleApiKey();
if (!apiKey) throw new Error("GOOGLE_API_KEY or GEMINI_API_KEY is required");
const url = buildGoogleUrl(pathname);
const proxy = getHttpProxy();
// When an HTTP proxy is detected, use curl instead of fetch.
// Bun's fetch has a known issue where long-lived connections through
// HTTP proxies get their sockets closed unexpectedly, causing image
// generation requests to fail with "socket connection was closed
// unexpectedly". Using curl as the HTTP client works around this.
if (proxy) {
return postGoogleJsonViaCurl<T>(url, apiKey, body);
}
return postGoogleJsonViaFetch<T>(url, apiKey, body);
}
function buildPromptWithAspect(
prompt: string,
ar: string | null,
quality: CliArgs["quality"],
): string {
let result = prompt;
if (ar) {
result += ` Aspect ratio: ${ar}.`;
@@ -86,7 +153,9 @@ function addAspectRatioToPrompt(prompt: string, ar: string | null): string {
return `${prompt} Aspect ratio: ${ar}.`;
}
async function readImageAsBase64(p: string): Promise<{ data: string; mimeType: string }> {
async function readImageAsBase64(
p: string,
): Promise<{ data: string; mimeType: string }> {
const buf = await readFile(p);
const ext = path.extname(p).toLowerCase();
let mimeType = "image/png";
@@ -97,7 +166,9 @@ async function readImageAsBase64(p: string): Promise<{ data: string; mimeType: s
}
function extractInlineImageData(response: {
candidates?: Array<{ content?: { parts?: Array<{ inlineData?: { data?: string } }> } }>;
candidates?: Array<{
content?: { parts?: Array<{ inlineData?: { data?: string } }> };
}>;
}): string | null {
for (const candidate of response.candidates || []) {
for (const part of candidate.content?.parts || []) {
@@ -112,16 +183,21 @@ function extractPredictedImageData(response: {
predictions?: Array<any>;
generatedImages?: Array<any>;
}): string | null {
const candidates = [...(response.predictions || []), ...(response.generatedImages || [])];
const candidates = [
...(response.predictions || []),
...(response.generatedImages || []),
];
for (const candidate of candidates) {
if (!candidate || typeof candidate !== "object") continue;
if (typeof candidate.imageBytes === "string") return candidate.imageBytes;
if (typeof candidate.bytesBase64Encoded === "string") return candidate.bytesBase64Encoded;
if (typeof candidate.bytesBase64Encoded === "string")
return candidate.bytesBase64Encoded;
if (typeof candidate.data === "string") return candidate.data;
const image = candidate.image;
if (image && typeof image === "object") {
if (typeof image.imageBytes === "string") return image.imageBytes;
if (typeof image.bytesBase64Encoded === "string") return image.bytesBase64Encoded;
if (typeof image.bytesBase64Encoded === "string")
return image.bytesBase64Encoded;
if (typeof image.data === "string") return image.data;
}
}
@@ -131,10 +207,13 @@ function extractPredictedImageData(response: {
async function generateWithGemini(
prompt: string,
model: string,
args: CliArgs
args: CliArgs,
): Promise<Uint8Array> {
const promptWithAspect = addAspectRatioToPrompt(prompt, args.aspectRatio);
const parts: Array<{ text?: string; inlineData?: { data: string; mimeType: string } }> = [];
const parts: Array<{
text?: string;
inlineData?: { data: string; mimeType: string };
}> = [];
for (const refPath of args.referenceImages) {
const { data, mimeType } = await readImageAsBase64(refPath);
parts.push({ inlineData: { data, mimeType } });
@@ -147,7 +226,9 @@ async function generateWithGemini(
console.log("Generating image with Gemini...", imageConfig);
const response = await postGoogleJson<{
candidates?: Array<{ content?: { parts?: Array<{ inlineData?: { data?: string } }> } }>;
candidates?: Array<{
content?: { parts?: Array<{ inlineData?: { data?: string } }> };
}>;
}>(`${toModelPath(model)}:generateContent`, {
contents: [
{
@@ -171,12 +252,18 @@ async function generateWithGemini(
async function generateWithImagen(
prompt: string,
model: string,
args: CliArgs
args: CliArgs,
): Promise<Uint8Array> {
const fullPrompt = buildPromptWithAspect(prompt, args.aspectRatio, args.quality);
const fullPrompt = buildPromptWithAspect(
prompt,
args.aspectRatio,
args.quality,
);
const imageSize = getGoogleImageSize(args);
if (imageSize === "4K") {
console.error("Warning: Imagen models do not support 4K imageSize, using 2K instead.");
console.error(
"Warning: Imagen models do not support 4K imageSize, using 2K instead.",
);
}
const parameters: Record<string, unknown> = {
@@ -212,17 +299,21 @@ async function generateWithImagen(
export async function generateImage(
prompt: string,
model: string,
args: CliArgs
args: CliArgs,
): Promise<Uint8Array> {
if (isGoogleImagen(model)) {
if (args.referenceImages.length > 0) {
console.error("Warning: Reference images not supported with Imagen models, ignoring.");
throw new Error(
"Reference images are not supported with Imagen models. Use gemini-3-pro-image-preview, gemini-3-flash-preview, or gemini-3.1-flash-image-preview.",
);
}
return generateWithImagen(prompt, model, args);
}
if (!isGoogleMultimodal(model) && args.referenceImages.length > 0) {
console.error("Warning: Reference images are only supported with Gemini multimodal models.");
throw new Error(
"Reference images are only supported with Gemini multimodal models. Use gemini-3-pro-image-preview, gemini-3-flash-preview, or gemini-3.1-flash-image-preview.",
);
}
return generateWithGemini(prompt, model, args);
@@ -1,9 +1,13 @@
import path from "node:path";
import { readFile } from "node:fs/promises";
import type { CliArgs } from "../types";
export function getDefaultModel(): string {
return process.env.OPENAI_IMAGE_MODEL || "gpt-image-1.5";
}
type OpenAIImageResponse = { data: Array<{ url?: string; b64_json?: string }> };
function parseAspectRatio(ar: string): { width: number; height: number } | null {
const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
if (!match) return null;
@@ -66,20 +70,70 @@ export async function generateImage(
if (!apiKey) throw new Error("OPENAI_API_KEY is required");
if (args.referenceImages.length > 0) {
console.error("Warning: Reference images not supported with OpenAI, ignoring.");
if (process.env.OPENAI_IMAGE_USE_CHAT === "true") {
return generateWithChatCompletions(baseURL, apiKey, prompt, model);
}
const size = args.size || getOpenAISize(model, args.aspectRatio, args.quality);
const body: Record<string, any> = {
model,
prompt,
size,
};
if (args.referenceImages.length > 0) {
if (model.includes("dall-e-2") || model.includes("dall-e-3")) {
throw new Error(
"Reference images with OpenAI in this skill require GPT Image models. Use --model gpt-image-1.5 (or another gpt-image model)."
);
}
return generateWithOpenAIEdits(baseURL, apiKey, prompt, model, size, args.referenceImages, args.quality);
}
return generateWithOpenAIGenerations(baseURL, apiKey, prompt, model, size, args.quality);
}
async function generateWithChatCompletions(
baseURL: string,
apiKey: string,
prompt: string,
model: string
): Promise<Uint8Array> {
const res = await fetch(`${baseURL}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
messages: [{ role: "user", content: prompt }],
}),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`OpenAI API error: ${err}`);
}
const result = (await res.json()) as { choices: Array<{ message: { content: string } }> };
const content = result.choices[0]?.message?.content ?? "";
const match = content.match(/data:image\/[^;]+;base64,([A-Za-z0-9+/=]+)/);
if (match) {
return Uint8Array.from(Buffer.from(match[1]!, "base64"));
}
throw new Error("No image found in chat completions response");
}
async function generateWithOpenAIGenerations(
baseURL: string,
apiKey: string,
prompt: string,
model: string,
size: string,
quality: CliArgs["quality"]
): Promise<Uint8Array> {
const body: Record<string, any> = { model, prompt, size };
if (model.includes("dall-e-3")) {
body.quality = args.quality === "2k" ? "hd" : "standard";
body.quality = quality === "2k" ? "hd" : "standard";
}
const res = await fetch(`${baseURL}/images/generations`, {
@@ -96,7 +150,62 @@ export async function generateImage(
throw new Error(`OpenAI API error: ${err}`);
}
const result = (await res.json()) as { data: Array<{ url?: string; b64_json?: string }> };
const result = (await res.json()) as OpenAIImageResponse;
return extractImageFromResponse(result);
}
async function generateWithOpenAIEdits(
baseURL: string,
apiKey: string,
prompt: string,
model: string,
size: string,
referenceImages: string[],
quality: CliArgs["quality"]
): Promise<Uint8Array> {
const form = new FormData();
form.append("model", model);
form.append("prompt", prompt);
form.append("size", size);
if (model.includes("gpt-image")) {
form.append("quality", quality === "2k" ? "high" : "medium");
}
for (const refPath of referenceImages) {
const bytes = await readFile(refPath);
const filename = path.basename(refPath);
const mimeType = getMimeType(filename);
const blob = new Blob([bytes], { type: mimeType });
form.append("image[]", blob, filename);
}
const res = await fetch(`${baseURL}/images/edits`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
},
body: form,
});
if (!res.ok) {
const err = await res.text();
throw new Error(`OpenAI edits API error: ${err}`);
}
const result = (await res.json()) as OpenAIImageResponse;
return extractImageFromResponse(result);
}
function getMimeType(filename: string): string {
const ext = path.extname(filename).toLowerCase();
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
if (ext === ".webp") return "image/webp";
if (ext === ".gif") return "image/gif";
return "image/png";
}
async function extractImageFromResponse(result: OpenAIImageResponse): Promise<Uint8Array> {
const img = result.data[0];
if (img?.b64_json) {
@@ -0,0 +1,203 @@
import path from "node:path";
import { readFile } from "node:fs/promises";
import type { CliArgs } from "../types";
const DEFAULT_MODEL = "google/nano-banana-pro";
const SYNC_WAIT_SECONDS = 60;
const POLL_INTERVAL_MS = 2000;
const MAX_POLL_MS = 300_000;
export function getDefaultModel(): string {
return process.env.REPLICATE_IMAGE_MODEL || DEFAULT_MODEL;
}
function getApiToken(): string | null {
return process.env.REPLICATE_API_TOKEN || null;
}
function getBaseUrl(): string {
const base = process.env.REPLICATE_BASE_URL || "https://api.replicate.com";
return base.replace(/\/+$/g, "");
}
function parseModelId(model: string): { owner: string; name: string; version: string | null } {
const [ownerName, version] = model.split(":");
const parts = ownerName!.split("/");
if (parts.length !== 2 || !parts[0] || !parts[1]) {
throw new Error(
`Invalid Replicate model format: "${model}". Expected "owner/name" or "owner/name:version".`
);
}
return { owner: parts[0], name: parts[1], version: version || null };
}
function buildInput(prompt: string, args: CliArgs, referenceImages: string[]): Record<string, unknown> {
const input: Record<string, unknown> = { prompt };
if (args.aspectRatio) {
input.aspect_ratio = args.aspectRatio;
}
if (args.n > 1) {
input.number_of_images = args.n;
}
input.output_format = "png";
if (referenceImages.length > 0) {
if (referenceImages.length === 1) {
input.image = referenceImages[0];
} else {
for (let i = 0; i < referenceImages.length; i++) {
input[`image${i > 0 ? i + 1 : ""}`] = referenceImages[i];
}
}
}
return input;
}
async function readImageAsDataUrl(p: string): Promise<string> {
const buf = await readFile(p);
const ext = path.extname(p).toLowerCase();
let mimeType = "image/png";
if (ext === ".jpg" || ext === ".jpeg") mimeType = "image/jpeg";
else if (ext === ".gif") mimeType = "image/gif";
else if (ext === ".webp") mimeType = "image/webp";
return `data:${mimeType};base64,${buf.toString("base64")}`;
}
type PredictionResponse = {
id: string;
status: string;
output: unknown;
error: string | null;
urls?: { get?: string };
};
async function createPrediction(
apiToken: string,
model: { owner: string; name: string; version: string | null },
input: Record<string, unknown>,
sync: boolean
): Promise<PredictionResponse> {
const baseUrl = getBaseUrl();
let url: string;
const body: Record<string, unknown> = { input };
if (model.version) {
url = `${baseUrl}/v1/predictions`;
body.version = model.version;
} else {
url = `${baseUrl}/v1/models/${model.owner}/${model.name}/predictions`;
}
const headers: Record<string, string> = {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
};
if (sync) {
headers["Prefer"] = `wait=${SYNC_WAIT_SECONDS}`;
}
const res = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Replicate API error (${res.status}): ${err}`);
}
return (await res.json()) as PredictionResponse;
}
async function pollPrediction(apiToken: string, getUrl: string): Promise<PredictionResponse> {
const start = Date.now();
while (Date.now() - start < MAX_POLL_MS) {
const res = await fetch(getUrl, {
headers: { Authorization: `Bearer ${apiToken}` },
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Replicate poll error (${res.status}): ${err}`);
}
const prediction = (await res.json()) as PredictionResponse;
if (prediction.status === "succeeded") return prediction;
if (prediction.status === "failed" || prediction.status === "canceled") {
throw new Error(`Replicate prediction ${prediction.status}: ${prediction.error || "unknown error"}`);
}
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
throw new Error(`Replicate prediction timed out after ${MAX_POLL_MS / 1000}s`);
}
function extractOutputUrl(prediction: PredictionResponse): string {
const output = prediction.output;
if (typeof output === "string") return output;
if (Array.isArray(output)) {
const first = output[0];
if (typeof first === "string") return first;
}
if (output && typeof output === "object" && "url" in output) {
const url = (output as Record<string, unknown>).url;
if (typeof url === "string") return url;
}
throw new Error(`Unexpected Replicate output format: ${JSON.stringify(output)}`);
}
async function downloadImage(url: string): Promise<Uint8Array> {
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to download image from Replicate: ${res.status}`);
const buf = await res.arrayBuffer();
return new Uint8Array(buf);
}
export async function generateImage(
prompt: string,
model: string,
args: CliArgs
): Promise<Uint8Array> {
const apiToken = getApiToken();
if (!apiToken) throw new Error("REPLICATE_API_TOKEN is required. Get one at https://replicate.com/account/api-tokens");
const parsedModel = parseModelId(model);
const refDataUrls: string[] = [];
for (const refPath of args.referenceImages) {
refDataUrls.push(await readImageAsDataUrl(refPath));
}
const input = buildInput(prompt, args, refDataUrls);
console.log(`Generating image with Replicate (${model})...`);
let prediction = await createPrediction(apiToken, parsedModel, input, true);
if (prediction.status !== "succeeded") {
if (!prediction.urls?.get) {
throw new Error("Replicate prediction did not return a poll URL");
}
console.log("Waiting for prediction to complete...");
prediction = await pollPrediction(apiToken, prediction.urls.get);
}
console.log("Generation completed.");
const outputUrl = extractOutputUrl(prediction);
return downloadImage(outputUrl);
}
+16 -2
View File
@@ -1,4 +1,4 @@
export type Provider = "google" | "openai" | "dashscope";
export type Provider = "google" | "openai" | "dashscope" | "replicate";
export type Quality = "normal" | "2k";
export type CliArgs = {
@@ -9,10 +9,24 @@ export type CliArgs = {
model: string | null;
aspectRatio: string | null;
size: string | null;
quality: Quality;
quality: Quality | null;
imageSize: string | null;
referenceImages: string[];
n: number;
json: boolean;
help: boolean;
};
export type ExtendConfig = {
version: number;
default_provider: Provider | null;
default_quality: Quality | null;
default_aspect_ratio: string | null;
default_image_size: "1K" | "2K" | "4K" | null;
default_model: {
google: string | null;
openai: string | null;
dashscope: string | null;
replicate: string | null;
};
};
+53 -18
View File
@@ -1,6 +1,6 @@
---
name: baoyu-infographic
description: Generates professional infographics with 20 layout types and 17 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics. Use when user asks to create "infographic", "信息图", "visual summary", or "可视化".
description: Generates professional infographics with 21 layout types and 20 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics. Use when user asks to create "infographic", "信息图", "visual summary", "可视化", or "高密度信息大图".
---
# Infographic Generator
@@ -20,8 +20,8 @@ Two dimensions: **layout** (information structure) × **style** (visual aestheti
| Option | Values |
|--------|--------|
| `--layout` | 20 options (see Layout Gallery), default: bento-grid |
| `--style` | 17 options (see Style Gallery), default: craft-handmade |
| `--layout` | 21 options (see Layout Gallery), default: bento-grid |
| `--style` | 20 options (see Style Gallery), default: craft-handmade |
| `--aspect` | landscape (16:9), portrait (9:16), square (1:1) |
| `--lang` | en, zh, ja, etc. |
@@ -49,6 +49,7 @@ Two dimensions: **layout** (information structure) × **style** (visual aestheti
| `venn-diagram` | Overlapping concepts |
| `winding-roadmap` | Journey, milestones |
| `circular-flow` | Cycles, recurring processes |
| `dense-modules` | High-density modules, data-rich guides |
Full definitions: `references/layouts/<layout>.md`
@@ -73,6 +74,9 @@ Full definitions: `references/layouts/<layout>.md`
| `ikea-manual` | Minimal line art |
| `knolling` | Organized flat-lay |
| `lego-brick` | Toy brick construction |
| `pop-laboratory` | Blueprint grid, coordinate markers, lab precision |
| `morandi-journal` | Hand-drawn doodle, warm Morandi tones |
| `retro-pop-grid` | 1970s retro pop art, Swiss grid, thick outlines |
Full definitions: `references/styles/<style>.md`
@@ -92,9 +96,23 @@ Full definitions: `references/styles/<style>.md`
| Educational | `bento-grid` + `chalkboard` |
| Journey | `winding-roadmap` + `storybook-watercolor` |
| Categories | `periodic-table` + `bold-graphic` |
| Product Guide | `dense-modules` + `morandi-journal` |
| Technical Guide | `dense-modules` + `pop-laboratory` |
| Trendy Guide | `dense-modules` + `retro-pop-grid` |
Default: `bento-grid` + `craft-handmade`
## Keyword Shortcuts
When user input contains these keywords, **auto-select** the associated layout and offer associated styles as top recommendations in Step 3. Skip content-based layout inference for matched keywords.
If a shortcut has **Prompt Notes**, append them to the generated prompt (Step 5) as additional style instructions.
| User Keyword | Layout | Recommended Styles | Default Aspect | Prompt Notes |
|--------------|--------|--------------------|----------------|--------------|
| 高密度信息大图 / high-density-info | `dense-modules` | `morandi-journal`, `pop-laboratory`, `retro-pop-grid` | portrait | — |
| 信息图 / infographic | `bento-grid` | `craft-handmade` | landscape | Minimalist: clean canvas, ample whitespace, no complex background textures. Simple cartoon elements and icons only. |
## Output Structure
```
@@ -110,7 +128,7 @@ Slug: 2-4 words kebab-case from topic. Conflict: append `-YYYYMMDD-HHMMSS`.
## Core Principles
- Preserve all source data **verbatim**—no summarization or rephrasing
- Preserve source data faithfully—no summarization or rephrasing (but **strip any credentials, API keys, tokens, or secrets** before including in outputs)
- Define learning objectives before structuring content
- Structure for visual communication (headlines, labels, visual elements)
@@ -120,16 +138,20 @@ Slug: 2-4 words kebab-case from topic. Conflict: append `-YYYYMMDD-HHMMSS`.
**1.1 Load Preferences (EXTEND.md)**
Use Bash to check EXTEND.md existence (priority order):
Check EXTEND.md existence (priority order):
```bash
# Check project-level first
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-infographic/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-infographic/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-infographic/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-infographic/EXTEND.md") { "user" }
```
┌────────────────────────────────────────────────────┬───────────────────┐
│ Path │ Location │
├────────────────────────────────────────────────────┼───────────────────┤
@@ -153,10 +175,12 @@ Schema: `references/config/preferences-schema.md`
**1.2 Analyze Content → `analysis.md`**
1. Save source content (file path or paste → `source.md`)
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
2. Analyze: topic, data type, complexity, tone, audience
3. Detect source language and user language
4. Extract design instructions from user input
5. Save analysis
- **Backup rule**: If `analysis.md` exists, rename to `analysis-backup-YYYYMMDD-HHMMSS.md`
See `references/analysis-framework.md` for detailed format.
@@ -168,13 +192,15 @@ Transform content into infographic structure:
3. Data points (all statistics/quotes copied exactly)
4. Design instructions from user
**Rules**: Markdown only. No new information. All data verbatim.
**Rules**: Markdown only. No new information. Preserve data faithfully. Strip any credentials or secrets from output.
See `references/structured-content-template.md` for detailed format.
### Step 3: Recommend Combinations
Recommend 3-5 layout×style combinations based on:
**3.1 Check Keyword Shortcuts first**: If user input matches a keyword from the **Keyword Shortcuts** table, auto-select the associated layout and prioritize associated styles as top recommendations. Skip content-based layout inference.
**3.2 Otherwise**, recommend 3-5 layout×style combinations based on:
- Data structure → matching layout
- Content tone → matching style
- Audience expectations
@@ -182,13 +208,20 @@ Recommend 3-5 layout×style combinations based on:
### Step 4: Confirm Options
Present all options in single confirmation:
1. **Combination** (always): 3+ options with rationale
2. **Aspect** (always): landscape/portrait/square
3. **Language** (only if source ≠ user language): which language for text
Use **single AskUserQuestion call** with multiple questions to confirm all options together:
| Question | When | Options |
|----------|------|---------|
| **Combination** | Always | 3+ layout×style combos with rationale |
| **Aspect** | Always | landscape (16:9), portrait (9:16), square (1:1) |
| **Language** | Only if source ≠ user language | Language for text content |
**Important**: Do NOT split into separate AskUserQuestion calls. Combine all applicable questions into one call.
### Step 5: Generate Prompt → `prompts/infographic.md`
**Backup rule**: If `prompts/infographic.md` exists, rename to `prompts/infographic-backup-YYYYMMDD-HHMMSS.md`
Combine:
1. Layout definition from `references/layouts/<layout>.md`
2. Style definition from `references/styles/<style>.md`
@@ -199,8 +232,10 @@ Combine:
### Step 6: Generate Image
1. Select available image generation skill (ask user if multiple)
2. Call with prompt file and output path
3. On failure, auto-retry once
2. **Check for existing file**: Before generating, check if `infographic.png` exists
- If exists: Rename to `infographic-backup-YYYYMMDD-HHMMSS.png`
3. Call with prompt file and output path
4. On failure, auto-retry once
### Step 7: Output Summary
@@ -211,8 +246,8 @@ Report: topic, layout, style, aspect, language, output path, files created.
- `references/analysis-framework.md` - Analysis methodology
- `references/structured-content-template.md` - Content format
- `references/base-prompt.md` - Prompt template
- `references/layouts/<layout>.md` - 20 layout definitions
- `references/styles/<style>.md` - 17 style definitions
- `references/layouts/<layout>.md` - 21 layout definitions
- `references/styles/<style>.md` - 20 style definitions
## Extension Support
@@ -38,7 +38,8 @@ Approach content analysis as a **world-class instructional designer**:
| **Cycle/Loop** | Recurring processes, feedback loops | circular-flow | craft-handmade, technical-schematic |
| **System/Structure** | Components, architecture, anatomy | structural-breakdown, bento-grid | technical-schematic, ikea-manual |
| **Journey/Narrative** | Stories, user flows, milestones | winding-roadmap, story-mountain | storybook-watercolor, comic-strip |
| **Overview/Summary** | Multiple topics, feature highlights | bento-grid, periodic-table | chalkboard, bold-graphic |
| **Overview/Summary** | Multiple topics, feature highlights | bento-grid, periodic-table, dense-modules | chalkboard, bold-graphic |
| **Product/Buying Guide** | Multi-dimension comparisons, specs, pitfalls | dense-modules | morandi-journal, pop-laboratory, retro-pop-grid |
### 2. Learning Objective Identification
@@ -0,0 +1,72 @@
# dense-modules
High-density modular layout with 6-7 typed information modules packed with concrete data.
## Structure
- 6-7 distinct modules per image, each serving a specific information function
- Every module contains concrete data: brand names, numbers, percentages, parameters
- Minimal whitespace—compact spacing prioritized over breathing room
- Smaller text acceptable to maximize information density
- Each module identified by coordinate label or section marker (e.g., MOD-1, SEC-A)
## Module Archetypes
| Module | Purpose | Content Requirements |
|--------|---------|---------------------|
| **Brand/Selection Array** | Grid of options with recommendations | 4-8 items with icons, names, brief descriptions; highlight "best choice" |
| **Specification Scale** | Quality/measurement gauge | 3-5 levels with precise numerical increments, quality indicators (emoji faces, checkmarks) |
| **Deep Dive/Detail** | Technical breakdown of key item | Zoom-in callouts, internal components, cross-section or exploded view |
| **Scenario Comparison** | Side-by-side use cases | 3-6 scenarios with specific recommendations and data per scenario |
| **Identification Tips** | How-to checklist | 3-5 inspection methods: look/test/check/ask format |
| **Warning/Pitfall Zone** | Critical mistakes to avoid | 3-5 pitfalls with consequences, 1-2 correct approaches; high visual contrast |
| **Quick Reference** | Compact summary | Dense table, one-line summaries, decision flowchart, or key takeaways |
## Variants
| Variant | Focus | Visual Emphasis |
|---------|-------|-----------------|
| **Coordinate-labeled** | Precision and systematicity | Each module has alphanumeric coordinate (A-01, B-05, C-12), ruler/axis markers |
| **Grid-cell** | Order and structure | Modules in strict rectangular cells divided by thick lines, Swiss grid feel |
| **Free-flowing** | Organic density | Magazine-style layout with dotted frames, varying module sizes, connected by arrows |
## Best For
- Product selection guides and buying guides
- Multi-dimensional comparison content
- Data-rich educational materials
- "Avoid pitfalls" / "complete guide" formats
- Content targeting platforms like Xiaohongshu with high-density visual requirements
## Visual Elements
- Module boundary markers (thick lines, dotted frames, or coordinate grids)
- Quality indicators per module (emoji faces, checkmarks, crosses, crowns)
- Data callout boxes with highlighted numbers
- Comparison arrows and progression indicators
- Warning/alert visual markers for pitfall modules
- Metadata in corners (page numbers, timestamps, small barcodes)
## Text Placement
- Main title at top, prominent and impactful
- Subtitle with module count ("X大维度全面解析...")
- Module headers inside colored badges or labeled frames
- Body text compact, multiple columns within modules
- Numbers highlighted with accent colors, slightly larger than body text
## Information Density Rules
- Every corner should contain useful information or metadata
- No decorative-only empty space
- Text size may be reduced to fit more content—information over font size
- Each module must have specific data points, not generic descriptions
- Balance between density and readability: dense but organized
## Recommended Pairings
- `pop-laboratory`: Technical precision with coordinate markers and blueprint grid
- `morandi-journal`: Hand-drawn warmth with doodle illustrations and organic frames
- `retro-pop-grid`: 1970s pop art with strict grid cells and bold contrast
- `corporate-memphis`: Clean business feel for product comparisons
- `technical-schematic`: Engineering precision for technical product guides
@@ -0,0 +1,60 @@
# morandi-journal
Hand-drawn doodle illustration with warm Morandi color tones and cozy bullet journal aesthetic.
## Color Palette
- Background: Warm cream/beige with subtle paper texture (#F5F0E6)
- Primary: Muted teal/sage green (#7BA3A8) for headers and frames
- Secondary: Warm terracotta/orange (#D4956A) for highlights and numbers
- Line art: Dark charcoal brown (#4A4540)
- Soft highlights: Pale yellow (#F5E6C8)
## Visual Elements
- Hand-drawn doodle illustrations with organic, slightly imperfect ink lines
- Washi tape strip decorations (diagonal stripes pattern, beige and brown)
- Rounded card containers for brand/option items
- Hand-drawn rulers, scales, and progress bars with emoji quality indicators
- Smiley/frowny faces as quality markers (😊✓ 😐 ☹️✗)
- Dotted line frames around sections
- Connecting arrows and dotted lines between modules
- Corner decorations: tiny houses, stars, sparkles, clouds
- Wavy line dividers between sections
- Callout bubbles for tips
- Magnifying glass icons for identification tips
- Thumbs up/down icons (hand-drawn style)
## Variants
| Variant | Focus | Visual Emphasis |
|---------|-------|-----------------|
| **Cozy journal** | Maximum warmth | More washi tape, stickers, decorative doodles |
| **Clean sketch** | Readability | Cleaner lines, less decoration, more structured |
## Typography
- Main title: Bold hand-lettered calligraphy style with decorative flourishes
- Module headers: Clean handwritten text in white on dark teal rounded badge (#6B9080)
- Body text: Neat handwritten print style, easy to read
- Numbers: Highlighted in terracotta (#D4956A), slightly larger than body
## Style Enforcement
- All imagery must maintain hand-drawn/doodle aesthetic—no digital precision
- Organic, slightly imperfect shapes throughout
- Sketch-like quality with visible line weight variations
- Warm and cozy journal feel, not clinical or corporate
## Avoid
- Flat vector icons or emoji
- Clean geometric shapes
- Stock illustration style
- Strict grid layout
- Pure white background
- Digital/corporate look
## Best For
Product selection guides, lifestyle content, educational overviews, consumer-facing comparison content, Xiaohongshu-style posts
@@ -0,0 +1,48 @@
# pop-laboratory
Lab manual precision meets pop art color impact—coordinate systems, technical diagrams, and fluorescent accents on blueprint grid.
## Color Palette
- Background: Professional grayish-white with faint blueprint grid texture (#F2F2F2)
- Primary: Muted teal/sage green (#B8D8BE) for major functional blocks and data zones
- High-alert accent: Vibrant fluorescent pink (#E91E63) strictly for warnings, critical data, or "winner" highlights
- Marker highlights: Vivid lemon yellow (#FFF200) as translucent highlighter effect for keywords
- Line art: Ultra-fine charcoal brown (#2D2926) for technical grids, coordinates, and hairlines
## Visual Elements
- Coordinate-style labels on every module (e.g., R-20, G-02, SEC-08)
- Technical diagrams: exploded views, cross-sections with anchor points, architectural skeletal lines
- Vertical/horizontal rulers with precise markers (0.5mm, 1.8mm, 45°)
- "Marker-over-print" effect: color blocks slightly offset from text, postmodern print feel
- Cross-hair targets, mathematical symbols (Σ, Δ, ∞), directional arrows (X/Y axis)
- Microscopic detail annotations alongside macroscopic bold headers
- Corner metadata: tiny barcodes, timestamps, technical parameters
- High contrast between massive bold headers and tiny 8pt-style annotations
## Typography
- Headers: Bold brutalist characters, high visual impact
- Body: Professional sans-serif or crisp technical print
- Numbers: Large, highlighted with yellow or blue to stand out
- Annotations: Ultra-crisp, small technical labels
## Style Enforcement
- Strictly systematic color usage: only teal, pink, yellow, charcoal—no rainbow palette
- Sufficient fine grid lines and coordinate annotations throughout
- Maintain tension between large impactful headers and small precise parameters
- Lab manual aesthetic: mix of microscopic details and macroscopic data
## Avoid
- Cute or cartoonish doodles
- Soft pastels or generic textures
- Empty white space
- Flat vector stock icons
- Organic or hand-drawn imperfections
## Best For
Technical product guides, specification comparisons, precision-focused data visualization, engineering-adjacent content
@@ -0,0 +1,47 @@
# retro-pop-grid
1970s retro pop art with strict Swiss international grid, thick black outlines, and flat color blocks.
## Color Palette
- Background: Warm vintage cream/beige (#F5F0E6)
- Flat accents: Salmon pink, sky blue, mustard yellow, mint green—all muted retro tones
- Contrast blocks: Solid pure black (#000000) and solid pure white (#FFFFFF) used strategically for extreme contrast
- Line art and outlines: Solid thick black
## Visual Elements
- Uniform thick black outlines on all illustrations, text boxes, and grid dividers
- Pure 2D flat vector aesthetic with subtle screen print texture
- Strict Swiss international grid: poster divided into square and rectangular cells by thick black lines
- Black-background cells with white text for warnings or key categories (inverted contrast)
- Geometric fill patterns in empty cells: checkerboards, diagonal lines, dots
- Flat abstract symbols, warning signs, keyholes, stars, arrows
- Vintage comic-style smiley/frowny faces for quality indicators
- Colored cells used for breathing room—some with minimal/no content
## Typography
- Headers: Bold brutalist or retro thick display fonts, high legibility
- Body: Clean sans-serif, structured typographic alignment
- Decorative English text acceptable for stylistic labels ("WARNING", "INFO", "BEST")
- All content text in specified language
## Style Enforcement
- Absolutely no gradients, shading, drop shadows, or 3D effects
- Everything anchored in grid cells—no floating or unorganized elements
- Maintain 1970s retro pop art and underground comic illustration feel
- Visual density balanced with rhythmic grid—some cells intentionally sparse for contrast
## Avoid
- 3D rendering, realistic details, gradients, soft shadows
- Soft, thin, or sketch-like pencil lines
- Free-flowing, unorganized, or floating layouts (everything must be grid-anchored)
- Pure white background canvas
- Organic or hand-drawn imperfections
## Best For
Trendy product guides, design-conscious content, visually striking comparisons, content targeting design-savvy audiences, bold social media posts
+64 -14
View File
@@ -9,7 +9,7 @@ Converts Markdown files to beautifully styled HTML with inline CSS, optimized fo
## 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 |
|--------|---------|
@@ -17,16 +17,20 @@ Converts Markdown files to beautifully styled HTML with inline CSS, optimized fo
## Preferences (EXTEND.md)
Use Bash to check EXTEND.md existence (priority order):
Check EXTEND.md existence (priority order):
```bash
# Check project-level first
# macOS, Linux, WSL, Git Bash
test -f .baoyu-skills/baoyu-markdown-to-html/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-markdown-to-html/EXTEND.md" && echo "user"
```
```powershell
# PowerShell (Windows)
if (Test-Path .baoyu-skills/baoyu-markdown-to-html/EXTEND.md) { "project" }
if (Test-Path "$HOME/.baoyu-skills/baoyu-markdown-to-html/EXTEND.md") { "user" }
```
┌──────────────────────────────────────────────────────────────┬───────────────────┐
│ Path │ Location │
├──────────────────────────────────────────────────────────────┼───────────────────┤
@@ -68,20 +72,41 @@ Use `AskUserQuestion` to ask whether to format first. Formatting can fix:
**If user declines**: Continue with original file.
### Step 1: Confirm Theme
### Step 1: Determine Theme
Before converting, use AskUserQuestion to confirm the theme (unless user already specified):
**Theme resolution order** (first match wins):
1. User explicitly specified theme (CLI `--theme` or conversation)
2. EXTEND.md `default_theme` (this skill's own EXTEND.md, checked in Step 0)
3. `baoyu-post-to-wechat` EXTEND.md `default_theme` (cross-skill fallback)
4. If none found → use AskUserQuestion to confirm
**Cross-skill EXTEND.md check** (only if this skill's EXTEND.md has no `default_theme`):
```bash
# Check baoyu-post-to-wechat EXTEND.md for default_theme
test -f "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md" && grep -o 'default_theme:.*' "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md"
```
```powershell
# PowerShell (Windows)
if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md") { Select-String -Pattern 'default_theme:.*' -Path "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md" | ForEach-Object { $_.Matches.Value } }
```
**If theme is resolved from EXTEND.md**: Use it directly, do NOT ask the user.
**If no default found**: Use AskUserQuestion to confirm:
| Theme | Description |
|-------|-------------|
| `default` (Recommended) | 经典主题 - 传统排版,标题居中带底边,二级标题白字彩底 |
| `grace` | 优雅主题 - 文字阴影,圆角卡片,精致引用块 |
| `simple` | 简洁主题 - 现代极简风,不对称圆角,清爽留白 |
| `modern` | 现代主题 - 大圆角、药丸形标题、宽松行距(搭配 `--color red` 还原传统红金风格) |
### Step 2: Convert
```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
@@ -91,32 +116,56 @@ Display the output path from JSON result. If backup was created, mention it.
## Usage
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts <markdown_file> [options]
${BUN_X} ${SKILL_DIR}/scripts/main.ts <markdown_file> [options]
```
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--theme <name>` | Theme name (default, grace, simple) | default |
| `--theme <name>` | Theme name (default, grace, simple, modern) | default |
| `--color <name\|hex>` | Primary color: preset name or hex value | theme default |
| `--font-family <name>` | Font: sans, serif, serif-cjk, mono, or CSS value | theme default |
| `--font-size <N>` | Font size: 14px, 15px, 16px, 17px, 18px | 16px |
| `--title <title>` | Override title from frontmatter | |
| `--keep-title` | Keep the first heading in content | false (removed) |
| `--help` | Show help | |
**Color Presets:**
| Name | Hex | Label |
|------|-----|-------|
| blue | #0F4C81 | 经典蓝 |
| green | #009874 | 翡翠绿 |
| vermilion | #FA5151 | 活力橘 |
| yellow | #FECE00 | 柠檬黄 |
| purple | #92617E | 薰衣紫 |
| sky | #55C9EA | 天空蓝 |
| rose | #B76E79 | 玫瑰金 |
| olive | #556B2F | 橄榄绿 |
| black | #333333 | 石墨黑 |
| gray | #A9A9A9 | 雾烟灰 |
| pink | #FFB7C5 | 樱花粉 |
| red | #A93226 | 中国红 |
| orange | #D97757 | 暖橘 (modern default) |
**Examples:**
```bash
# 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
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
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --theme modern --color red
# 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
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
@@ -154,6 +203,7 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --title "My Article"
| `default` | 经典主题 - 传统排版,标题居中带底边,二级标题白字彩底 |
| `grace` | 优雅主题 - 文字阴影,圆角卡片,精致引用块 (by @brzhang) |
| `simple` | 简洁主题 - 现代极简风,不对称圆角,清爽留白 (by @okooo5km) |
| `modern` | 现代主题 - 大圆角、药丸形标题、宽松行距(搭配 `--color red` 还原传统红金风格) |
## Supported Markdown Features
+51 -33
View File
@@ -1,13 +1,17 @@
import fs from 'node:fs';
import path from 'node:path';
import { writeFile } from 'node:fs/promises';
import os from 'node:os';
import { createHash } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import https from 'node:https';
import http from 'node:http';
import { spawnSync } from 'node:child_process';
import process from 'node:process';
import type { StyleConfig, HtmlDocumentMeta } from './md/types.js';
import { DEFAULT_STYLE, THEME_STYLE_DEFAULTS } from './md/constants.js';
import { loadThemeCss, normalizeThemeCss } from './md/themes.js';
import { initRenderer, renderMarkdown, postProcessHtml } from './md/renderer.js';
import {
buildCss, loadCodeThemeCss, buildHtmlDocument,
inlineCss, normalizeInlineCss, modifyHtmlStructure, removeFirstHeading,
} from './md/html-builder.js';
interface ImageInfo {
placeholder: string;
@@ -126,7 +130,18 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
const { frontmatter, body } = parseFrontmatter(content);
let title = options?.title ?? frontmatter.title ?? '';
const stripQuotes = (s?: string): string => {
if (!s) return '';
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
return s.slice(1, -1);
}
if ((s.startsWith('\u201c') && s.endsWith('\u201d')) || (s.startsWith('\u2018') && s.endsWith('\u2019'))) {
return s.slice(1, -1);
}
return s;
};
let title = options?.title ?? stripQuotes(frontmatter.title) ?? '';
if (!title) {
const lines = body.split('\n');
for (const line of lines) {
@@ -138,8 +153,8 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
}
}
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
const author = frontmatter.author || '';
let summary = frontmatter.description || frontmatter.summary || '';
const author = stripQuotes(frontmatter.author);
let summary = stripQuotes(frontmatter.description) || stripQuotes(frontmatter.summary);
if (!summary) {
const lines = body.split('\n');
@@ -176,33 +191,23 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
const modifiedMarkdown = `---\n${Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`).join('\n')}\n---\n${modifiedBody}`;
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'markdown-to-html-'));
const tempMdPath = path.join(tempDir, 'temp-article.md');
await writeFile(tempMdPath, modifiedMarkdown, 'utf-8');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const renderScript = path.join(__dirname, 'md', 'render.ts');
console.error(`[markdown-to-html] Rendering with theme: ${theme}, keepTitle: ${keepTitle}`);
const args = ['-y', 'bun', renderScript, tempMdPath, '--theme', theme];
if (keepTitle) args.push('--keep-title');
const themeDefaults = THEME_STYLE_DEFAULTS[theme] ?? {};
const style: StyleConfig = { ...DEFAULT_STYLE, ...themeDefaults };
const { baseCss, themeCss } = loadThemeCss(theme);
const css = normalizeThemeCss(buildCss(baseCss, themeCss, style));
const codeThemeCss = loadCodeThemeCss('github');
const result = spawnSync('npx', args, {
stdio: ['inherit', 'pipe', 'pipe'],
cwd: baseDir,
});
const renderer = initRenderer({});
const { html: baseHtml, readingTime } = renderMarkdown(modifiedMarkdown, renderer);
let htmlContent = postProcessHtml(baseHtml, readingTime, renderer);
if (!keepTitle) htmlContent = removeFirstHeading(htmlContent);
if (result.status !== 0) {
const stderr = result.stderr?.toString() || '';
throw new Error(`Render failed: ${stderr}`);
}
const tempHtmlPath = tempMdPath.replace(/\.md$/i, '.html');
if (!fs.existsSync(tempHtmlPath)) {
throw new Error(`HTML file not generated: ${tempHtmlPath}`);
}
const meta: HtmlDocumentMeta = { title, author, description: summary };
const fullHtml = buildHtmlDocument(meta, css, htmlContent, codeThemeCss);
const inlinedHtml = normalizeInlineCss(await inlineCss(fullHtml), style);
const renderedHtml = modifyHtmlStructure(inlinedHtml);
const finalHtmlPath = markdownPath.replace(/\.md$/i, '.html');
let backupPath: string | undefined;
@@ -213,12 +218,16 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
fs.renameSync(finalHtmlPath, backupPath);
}
fs.copyFileSync(tempHtmlPath, finalHtmlPath);
console.error(`[markdown-to-html] HTML saved to: ${finalHtmlPath}`);
fs.writeFileSync(finalHtmlPath, renderedHtml, 'utf-8');
const contentImages: ImageInfo[] = [];
let tempDir: string | undefined;
for (const img of images) {
const localPath = await resolveImagePath(img.src, baseDir, tempDir);
if (!tempDir && (img.src.startsWith('http://') || img.src.startsWith('https://'))) {
const os = await import('node:os');
tempDir = fs.mkdtempSync(path.join(os.default.tmpdir(), 'markdown-to-html-'));
}
const localPath = await resolveImagePath(img.src, baseDir, tempDir ?? baseDir);
contentImages.push({
placeholder: img.placeholder,
localPath,
@@ -226,6 +235,15 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
});
}
let finalContent = fs.readFileSync(finalHtmlPath, 'utf-8');
for (const img of contentImages) {
const imgTag = `<img src="${img.originalPath}" data-local-path="${img.localPath}" style="display: block; width: 100%; margin: 1.5em auto;">`;
finalContent = finalContent.replace(img.placeholder, imgTag);
}
fs.writeFileSync(finalHtmlPath, finalContent, 'utf-8');
console.error(`[markdown-to-html] HTML saved to: ${finalHtmlPath}`);
return {
title,
author,
@@ -0,0 +1,163 @@
import type { CliOptions, ThemeName } from "./types.js";
import {
FONT_FAMILY_MAP,
FONT_SIZE_OPTIONS,
COLOR_PRESETS,
CODE_BLOCK_THEMES,
} from "./constants.js";
import { THEME_NAMES } from "./themes.js";
import { loadExtendConfig } from "./extend-config.js";
export function printUsage(): void {
console.error(
[
"Usage:",
" npx tsx src/md/render.ts <markdown_file> [options]",
"",
"Options:",
` --theme <name> Theme (${THEME_NAMES.join(", ")})`,
` --color <name|hex> Primary color: ${Object.keys(COLOR_PRESETS).join(", ")}, or hex`,
` --font-family <name> Font: ${Object.keys(FONT_FAMILY_MAP).join(", ")}, or CSS value`,
` --font-size <N> Font size: ${FONT_SIZE_OPTIONS.join(", ")} (default: 16px)`,
` --code-theme <name> Code highlight theme (default: github)`,
` --mac-code-block Show Mac-style code block header`,
` --line-number Show line numbers in code blocks`,
` --cite Enable footnote citations`,
` --count Show reading time / word count`,
` --legend <value> Image caption: title-alt, alt-title, title, alt, none`,
` --keep-title Keep the first heading in output`,
].join("\n")
);
}
function parseArgValue(argv: string[], i: number, flag: string): string | null {
const arg = argv[i]!;
if (arg.includes("=")) {
return arg.slice(flag.length + 1);
}
const next = argv[i + 1];
return next ?? null;
}
function resolveFontFamily(value: string): string {
return FONT_FAMILY_MAP[value] ?? value;
}
function resolveColor(value: string): string {
return COLOR_PRESETS[value] ?? value;
}
export function parseArgs(argv: string[]): CliOptions | null {
const ext = loadExtendConfig();
let inputPath = "";
let theme: ThemeName = ext.default_theme ?? "default";
let keepTitle = ext.keep_title ?? false;
let primaryColor: string | undefined = ext.default_color ? resolveColor(ext.default_color) : undefined;
let fontFamily: string | undefined = ext.default_font_family ? resolveFontFamily(ext.default_font_family) : undefined;
let fontSize: string | undefined = ext.default_font_size ?? undefined;
let codeTheme = ext.default_code_theme ?? "github";
let isMacCodeBlock = ext.mac_code_block ?? true;
let isShowLineNumber = ext.show_line_number ?? false;
let citeStatus = ext.cite ?? false;
let countStatus = ext.count ?? false;
let legend = ext.legend ?? "alt";
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i]!;
if (!arg.startsWith("--") && !inputPath) {
inputPath = arg;
continue;
}
if (arg === "--help" || arg === "-h") {
return null;
}
if (arg === "--keep-title") { keepTitle = true; continue; }
if (arg === "--mac-code-block") { isMacCodeBlock = true; continue; }
if (arg === "--no-mac-code-block") { isMacCodeBlock = false; continue; }
if (arg === "--line-number") { isShowLineNumber = true; continue; }
if (arg === "--cite") { citeStatus = true; continue; }
if (arg === "--count") { countStatus = true; continue; }
if (arg === "--theme" || arg.startsWith("--theme=")) {
const val = parseArgValue(argv, i, "--theme");
if (!val) { console.error("Missing value for --theme"); return null; }
theme = val as ThemeName;
if (!arg.includes("=")) i += 1;
continue;
}
if (arg === "--color" || arg.startsWith("--color=")) {
const val = parseArgValue(argv, i, "--color");
if (!val) { console.error("Missing value for --color"); return null; }
primaryColor = resolveColor(val);
if (!arg.includes("=")) i += 1;
continue;
}
if (arg === "--font-family" || arg.startsWith("--font-family=")) {
const val = parseArgValue(argv, i, "--font-family");
if (!val) { console.error("Missing value for --font-family"); return null; }
fontFamily = resolveFontFamily(val);
if (!arg.includes("=")) i += 1;
continue;
}
if (arg === "--font-size" || arg.startsWith("--font-size=")) {
const val = parseArgValue(argv, i, "--font-size");
if (!val) { console.error("Missing value for --font-size"); return null; }
fontSize = val.endsWith("px") ? val : `${val}px`;
if (!FONT_SIZE_OPTIONS.includes(fontSize)) {
console.error(`Invalid font size: ${fontSize}. Valid: ${FONT_SIZE_OPTIONS.join(", ")}`);
return null;
}
if (!arg.includes("=")) i += 1;
continue;
}
if (arg === "--code-theme" || arg.startsWith("--code-theme=")) {
const val = parseArgValue(argv, i, "--code-theme");
if (!val) { console.error("Missing value for --code-theme"); return null; }
codeTheme = val;
if (!CODE_BLOCK_THEMES.includes(codeTheme)) {
console.error(`Unknown code theme: ${codeTheme}`);
return null;
}
if (!arg.includes("=")) i += 1;
continue;
}
if (arg === "--legend" || arg.startsWith("--legend=")) {
const val = parseArgValue(argv, i, "--legend");
if (!val) { console.error("Missing value for --legend"); return null; }
const valid = ["title-alt", "alt-title", "title", "alt", "none"];
if (!valid.includes(val)) {
console.error(`Invalid legend: ${val}. Valid: ${valid.join(", ")}`);
return null;
}
legend = val;
if (!arg.includes("=")) i += 1;
continue;
}
console.error(`Unknown argument: ${arg}`);
return null;
}
if (!inputPath) {
return null;
}
if (!THEME_NAMES.includes(theme)) {
console.error(`Unknown theme: ${theme}`);
return null;
}
return {
inputPath, theme, keepTitle, primaryColor, fontFamily, fontSize,
codeTheme, isMacCodeBlock, isShowLineNumber, citeStatus, countStatus, legend,
};
}
@@ -0,0 +1,9 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: 1c-light
Description: Style IDE 1C:Enterprise 8
Author: (c) Barilko Vitaliy <barilkovetal@gmail.com>
Maintainer: @Diversus23
Website: https://softonit.ru/
License: see project LICENSE
Touched: 2023
*/.hljs{color:#00f;background:#fff}.hljs-comment{color:green}.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-function,.hljs-keyword,.hljs-name,.hljs-punctuation,.hljs-selector-tag{color:red}.hljs-params,.hljs-type{color:#00f}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-symbol,.hljs-template-tag{color:#000}.hljs-section,.hljs-title{color:#00f}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:red}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#00f}.hljs-meta,.hljs-meta .hljs-keyword,.hljs-meta .hljs-string{color:#963200}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
@@ -0,0 +1,7 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: a11y-dark
Author: @ericwbailey
Maintainer: @ericwbailey
Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
*/.hljs{background:#2b2b2b;color:#f8f8f2}.hljs-comment,.hljs-quote{color:#d4d0ab}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#ffa07a}.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#f5ab35}.hljs-attribute{color:gold}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:#abe338}.hljs-section,.hljs-title{color:#00e0e0}.hljs-keyword,.hljs-selector-tag{color:#dcc6e0}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-quote,.hljs-string,.hljs-symbol,.hljs-type{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}}
@@ -0,0 +1,7 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: a11y-light
Author: @ericwbailey
Maintainer: @ericwbailey
Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
*/.hljs{background:#fefefe;color:#545454}.hljs-comment,.hljs-quote{color:#696969}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#d91e18}.hljs-attribute,.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#aa5d00}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:green}.hljs-section,.hljs-title{color:#007faa}.hljs-keyword,.hljs-selector-tag{color:#7928a1}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-quote,.hljs-string,.hljs-symbol,.hljs-type{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}}
@@ -0,0 +1,20 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: Agate
Author: (c) Taufik Nurrohman <hi@taufik-nurrohman.com>
Maintainer: @taufik-nurrohman
Updated: 2021-04-24
#333
#62c8f3
#7bd694
#888
#a2fca2
#ade5fc
#b8d8a2
#c6b4f0
#d36363
#fc9b9b
#fcc28c
#ffa
#fff
*/.hljs{background:#333;color:#fff}.hljs-doctag,.hljs-meta-keyword,.hljs-name,.hljs-strong{font-weight:700}.hljs-code,.hljs-emphasis{font-style:italic}.hljs-section,.hljs-tag{color:#62c8f3}.hljs-selector-class,.hljs-selector-id,.hljs-template-variable,.hljs-variable{color:#ade5fc}.hljs-meta-string,.hljs-string{color:#a2fca2}.hljs-attr,.hljs-quote,.hljs-selector-attr{color:#7bd694}.hljs-tag .hljs-attr{color:inherit}.hljs-attribute,.hljs-title,.hljs-type{color:#ffa}.hljs-number,.hljs-symbol{color:#d36363}.hljs-bullet,.hljs-template-tag{color:#b8d8a2}.hljs-built_in,.hljs-keyword,.hljs-literal,.hljs-selector-tag{color:#fcc28c}.hljs-code,.hljs-comment,.hljs-formula{color:#888}.hljs-link,.hljs-regexp,.hljs-selector-pseudo{color:#c6b4f0}.hljs-meta{color:#fc9b9b}.hljs-deletion{background:#fc9b9b;color:#333}.hljs-addition{background:#a2fca2;color:#333}.hljs-subst{color:#fff}.hljs a{color:inherit}.hljs a:focus,.hljs a:hover{color:inherit;text-decoration:underline}.hljs mark{background:#555;color:inherit}
@@ -0,0 +1,9 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: An Old Hope Star Wars Syntax
Author: (c) Gustavo Costa <gusbemacbe@gmail.com>
Maintainer: @gusbemacbe
Original theme - Ocean Dark Theme by https://github.com/gavsiu
Based on Jesse Leite's Atom syntax theme 'An Old Hope'
https://github.com/JesseLeite/an-old-hope-syntax-atom
*/.hljs{background:#1c1d21;color:#c0c5ce}.hljs-comment,.hljs-quote{color:#b6b18b}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#eb3c54}.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#e7ce56}.hljs-attribute{color:#ee7c2b}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:#4fb4d7}.hljs-section,.hljs-title{color:#78bb65}.hljs-keyword,.hljs-selector-tag{color:#b45ea4}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#a9b7c6;background:#282b2e}.hljs-bullet,.hljs-literal,.hljs-number,.hljs-symbol{color:#6897bb}.hljs-deletion,.hljs-keyword,.hljs-selector-tag{color:#cc7832}.hljs-link,.hljs-template-variable,.hljs-variable{color:#629755}.hljs-comment,.hljs-quote{color:grey}.hljs-meta{color:#bbb529}.hljs-addition,.hljs-attribute,.hljs-string{color:#6a8759}.hljs-section,.hljs-title,.hljs-type{color:#ffc66d}.hljs-name,.hljs-selector-class,.hljs-selector-id{color:#e8bf6a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#fff;color:#434f54}.hljs-subst{color:#434f54}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-name,.hljs-selector-tag{color:#00979d}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code,.hljs-literal{color:#d35400}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#00979d}.hljs-deletion,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#005c5f}.hljs-comment{color:rgba(149,165,166,.8)}.hljs-meta .hljs-keyword{color:#728e00}.hljs-meta{color:#434f54}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-function{color:#728e00}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-number{color:#8a7b52}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#222;color:#aaa}.hljs-subst{color:#aaa}.hljs-section{color:#fff}.hljs-comment,.hljs-meta,.hljs-quote{color:#444}.hljs-bullet,.hljs-regexp,.hljs-string,.hljs-symbol{color:#fc3}.hljs-addition,.hljs-number{color:#0c6}.hljs-attribute,.hljs-built_in,.hljs-link,.hljs-literal,.hljs-template-variable,.hljs-type{color:#32aaee}.hljs-keyword,.hljs-name,.hljs-selector-class,.hljs-selector-id,.hljs-selector-tag{color:#64a}.hljs-deletion,.hljs-template-tag,.hljs-title,.hljs-variable{color:#b16}.hljs-doctag,.hljs-section,.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#fff;color:#000}.hljs-addition,.hljs-attribute,.hljs-bullet,.hljs-link,.hljs-section,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#888}.hljs-comment,.hljs-deletion,.hljs-meta,.hljs-quote{color:#ccc}.hljs-keyword,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-strong,.hljs-type{font-weight:700}.hljs-emphasis{font-style:italic}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-keyword,.hljs-operator,.hljs-pattern-match{color:#f92672}.hljs-function,.hljs-pattern-match .hljs-constructor{color:#61aeee}.hljs-function .hljs-params{color:#a6e22e}.hljs-function .hljs-params .hljs-typing{color:#fd971f}.hljs-module-access .hljs-module{color:#7e57c2}.hljs-constructor{color:#e2b93d}.hljs-constructor .hljs-string{color:#9ccc65}.hljs-comment,.hljs-quote{color:#b18eb1;font-style:italic}.hljs-doctag,.hljs-formula{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#363c69;background:url(./brown-papersq.png) #b7a68e}.hljs-keyword,.hljs-literal,.hljs-selector-tag{color:#059}.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-link,.hljs-name,.hljs-section,.hljs-string,.hljs-symbol,.hljs-template-tag,.hljs-template-variable,.hljs-title,.hljs-type,.hljs-variable{color:#2c009f}.hljs-comment,.hljs-deletion,.hljs-meta,.hljs-quote{color:#802022}.hljs-doctag,.hljs-keyword,.hljs-literal,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-strong,.hljs-title,.hljs-type{font-weight:700}.hljs-emphasis{font-style:italic}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#222;color:#fff}.hljs-comment,.hljs-quote{color:#777}.hljs-built_in,.hljs-bullet,.hljs-deletion,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-regexp,.hljs-symbol,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#ab875d}.hljs-attribute,.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-title,.hljs-type{color:#9b869b}.hljs-addition,.hljs-keyword,.hljs-selector-tag,.hljs-string{color:#8f9c6c}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#000;background:#fff}.hljs-addition,.hljs-meta,.hljs-string,.hljs-symbol,.hljs-template-tag,.hljs-template-variable{color:#756bb1}.hljs-comment,.hljs-quote{color:#636363}.hljs-bullet,.hljs-link,.hljs-literal,.hljs-number,.hljs-regexp{color:#31a354}.hljs-deletion,.hljs-variable{color:#88f}.hljs-built_in,.hljs-doctag,.hljs-keyword,.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-selector-tag,.hljs-strong,.hljs-tag,.hljs-title,.hljs-type{color:#3182bd}.hljs-emphasis{font-style:italic}.hljs-attribute{color:#e6550d}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#ddd;background:#303030}.hljs-keyword,.hljs-link,.hljs-literal,.hljs-section,.hljs-selector-tag{color:#fff}.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-name,.hljs-string,.hljs-symbol,.hljs-template-tag,.hljs-template-variable,.hljs-title,.hljs-type,.hljs-variable{color:#d88}.hljs-comment,.hljs-deletion,.hljs-meta,.hljs-quote{color:#979797}.hljs-doctag,.hljs-keyword,.hljs-literal,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-strong,.hljs-title,.hljs-type{font-weight:700}.hljs-emphasis{font-style:italic}
@@ -0,0 +1,9 @@
/*!
Theme: Default
Description: Original highlight.js style
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
Maintainer: @highlightjs/core-team
Website: https://highlightjs.org/
License: see project LICENSE
Touched: 2021
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
@@ -0,0 +1,7 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: devibeans (dark)
Author: @terminaldweller
Maintainer: @terminaldweller
Inspired by vim's jellybeans theme (https://github.com/nanotech/jellybeans.vim)
*/.hljs{background:#000;color:#a39e9b}.hljs-attr,.hljs-template-tag{color:#8787d7}.hljs-comment,.hljs-doctag,.hljs-quote{color:#396}.hljs-params{color:#a39e9b}.hljs-regexp{color:#d700ff}.hljs-literal,.hljs-number,.hljs-selector-id,.hljs-tag{color:#ef5350}.hljs-meta,.hljs-meta .hljs-keyword{color:#0087ff}.hljs-code,.hljs-formula,.hljs-keyword,.hljs-link,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-variable{color:#64b5f6}.hljs-built_in,.hljs-deletion,.hljs-title{color:#ff8700}.hljs-attribute,.hljs-function,.hljs-name,.hljs-property,.hljs-section,.hljs-type{color:#ffd75f}.hljs-addition,.hljs-bullet,.hljs-meta .hljs-string,.hljs-string,.hljs-subst,.hljs-symbol{color:#558b2f}.hljs-selector-tag{color:#96f}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#000;background:#f8f8ff}.hljs-comment,.hljs-quote{color:#408080;font-style:italic}.hljs-keyword,.hljs-literal,.hljs-selector-tag,.hljs-subst{color:#954121}.hljs-number{color:#40a070}.hljs-doctag,.hljs-string{color:#219161}.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-type{color:#19469d}.hljs-params{color:#00f}.hljs-title{color:#458;font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-template-variable,.hljs-variable{color:teal}.hljs-link,.hljs-regexp{color:#b68}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#0ff;background:navy}.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-string,.hljs-symbol,.hljs-template-tag,.hljs-template-variable{color:#ff0}.hljs-keyword,.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-selector-tag,.hljs-type,.hljs-variable{color:#fff}.hljs-comment,.hljs-deletion,.hljs-doctag,.hljs-quote{color:#888}.hljs-link,.hljs-literal,.hljs-number,.hljs-regexp{color:#0f0}.hljs-meta{color:teal}.hljs-keyword,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-strong,.hljs-title{font-weight:700}.hljs-emphasis{font-style:italic}
@@ -0,0 +1,7 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
* Theme: FelipeC
* Author: (c) 2021 Felipe Contreras <felipe.contreras@gmail.com>
* Website: https://github.com/felipec/vim-felipec
*
* Autogenerated with vim-felipec's generator.
*/.hljs{color:#dedde4;background-color:#1d1c21}.hljs ::selection,.hljs::selection{color:#1d1c21;background-color:#ba9cef}.hljs-code,.hljs-comment,.hljs-quote{color:#9e9da4}.hljs-deletion,.hljs-literal,.hljs-number{color:#f09080}.hljs-doctag,.hljs-meta,.hljs-operator,.hljs-punctuation,.hljs-selector-attr,.hljs-subst,.hljs-template-variable{color:#ffbb7b}.hljs-type{color:#fddb7c}.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-title{color:#c4da7d}.hljs-addition,.hljs-regexp,.hljs-string{color:#93e4a4}.hljs-class,.hljs-property{color:#65e7d1}.hljs-name,.hljs-selector-tag{color:#30c2d8}.hljs-built_in,.hljs-keyword{color:#5fb8f2}.hljs-bullet,.hljs-section{color:#90aafa}.hljs-selector-pseudo{color:#ba9cef}.hljs-attr,.hljs-attribute,.hljs-params,.hljs-variable{color:#d991d2}.hljs-link,.hljs-symbol{color:#ec8dab}.hljs-literal,.hljs-strong,.hljs-title{font-weight:700}.hljs-emphasis{font-style:italic}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#eee;color:#000}.hljs-addition,.hljs-attribute,.hljs-emphasis,.hljs-link{color:#070}.hljs-emphasis{font-style:italic}.hljs-deletion,.hljs-string,.hljs-strong{color:#d14}.hljs-strong{font-weight:700}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-section,.hljs-title{color:#900}.hljs-class .hljs-title,.hljs-title.class_,.hljs-type{color:#458}.hljs-template-variable,.hljs-variable{color:#369}.hljs-bullet{color:#970}.hljs-meta{color:#34b}.hljs-code,.hljs-keyword,.hljs-literal,.hljs-number,.hljs-selector-tag{color:#099}.hljs-regexp{background-color:#fff0ff;color:#808}.hljs-symbol{color:#990073}.hljs-name,.hljs-selector-class,.hljs-selector-id,.hljs-tag{color:#070}
@@ -0,0 +1,9 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub Dark Dimmed
Description: Dark dimmed theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Colors taken from GitHub's CSS
*/.hljs{color:#adbac7;background:#22272e}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#f47067}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#dcbdfb}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#6cb6ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#96d0ff}.hljs-built_in,.hljs-symbol{color:#f69d50}.hljs-code,.hljs-comment,.hljs-formula{color:#768390}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#8ddb8c}.hljs-subst{color:#adbac7}.hljs-section{color:#316dca;font-weight:700}.hljs-bullet{color:#eac55f}.hljs-emphasis{color:#adbac7;font-style:italic}.hljs-strong{color:#adbac7;font-weight:700}.hljs-addition{color:#b4f1b4;background-color:#1b4721}.hljs-deletion{color:#ffd8d3;background-color:#78191b}
@@ -0,0 +1,10 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub Dark
Description: Dark theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-dark
Current colors taken from GitHub's CSS
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
@@ -0,0 +1,10 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#222;color:silver}.hljs-keyword{color:#ffb871;font-weight:700}.hljs-built_in{color:#ffb871}.hljs-literal{color:#ff8080}.hljs-symbol{color:#58e55a}.hljs-comment{color:#5b995b}.hljs-string{color:#ff0}.hljs-number{color:#ff8080}.hljs-addition,.hljs-attribute,.hljs-bullet,.hljs-code,.hljs-deletion,.hljs-doctag,.hljs-function,.hljs-link,.hljs-meta,.hljs-meta .hljs-keyword,.hljs-name,.hljs-quote,.hljs-regexp,.hljs-section,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-selector-tag,.hljs-subst,.hljs-template-tag,.hljs-template-variable,.hljs-title,.hljs-type,.hljs-variable{color:silver}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#fff;color:#000}.hljs-comment,.hljs-quote{color:#800}.hljs-keyword,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-title{color:#008}.hljs-template-variable,.hljs-variable{color:#660}.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-string{color:#080}.hljs-bullet,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-symbol{color:#066}.hljs-attr,.hljs-built_in,.hljs-doctag,.hljs-params,.hljs-title,.hljs-type{color:#606}.hljs-attribute,.hljs-subst{color:#000}.hljs-formula{background-color:#eee;font-style:italic}.hljs-selector-class,.hljs-selector-id{color:#9b703f}.hljs-addition{background-color:#baeeba}.hljs-deletion{background-color:#ffc8bd}.hljs-doctag,.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background-color:#652487;background-image:linear-gradient(160deg,#652487 0,#443ac3 35%,#0174b7 68%,#04988e 100%);color:#e7e4eb}.hljs-subtr{color:#e7e4eb}.hljs-comment,.hljs-doctag,.hljs-meta,.hljs-quote{color:#af8dd9}.hljs-attr,.hljs-regexp,.hljs-selector-id,.hljs-selector-tag,.hljs-tag,.hljs-template-tag{color:#aefbff}.hljs-bullet,.hljs-params,.hljs-selector-class{color:#f19fff}.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-symbol,.hljs-type{color:#17fc95}.hljs-addition,.hljs-link,.hljs-number{color:#c5fe00}.hljs-string{color:#38c0ff}.hljs-addition,.hljs-attribute{color:#e7ff9f}.hljs-template-variable,.hljs-variable{color:#e447ff}.hljs-built_in,.hljs-class,.hljs-formula,.hljs-function,.hljs-name,.hljs-title{color:#ffc800}.hljs-deletion,.hljs-literal,.hljs-selector-pseudo{color:#ff9e44}.hljs-emphasis,.hljs-quote{font-style:italic}.hljs-keyword,.hljs-params,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-selector-tag,.hljs-strong,.hljs-template-tag{font-weight:700}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background-color:#f9ccff;background-image:linear-gradient(295deg,#f9ccff 0,#e6bbf9 11%,#9ec6f9 32%,#55e6ee 60%,#91f5d1 74%,#f9ffbf 98%);color:#250482}.hljs-subtr{color:#01958b}.hljs-comment,.hljs-doctag,.hljs-meta,.hljs-quote{color:#cb7200}.hljs-attr,.hljs-regexp,.hljs-selector-id,.hljs-selector-tag,.hljs-tag,.hljs-template-tag{color:#07bd5f}.hljs-bullet,.hljs-params,.hljs-selector-class{color:#43449f}.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-symbol,.hljs-type{color:#7d2801}.hljs-addition,.hljs-link,.hljs-number{color:#7f0096}.hljs-string{color:#2681ab}.hljs-addition,.hljs-attribute{color:#296562}.hljs-template-variable,.hljs-variable{color:#025c8f}.hljs-built_in,.hljs-class,.hljs-formula,.hljs-function,.hljs-name,.hljs-title{color:#529117}.hljs-deletion,.hljs-literal,.hljs-selector-pseudo{color:#ad13ff}.hljs-emphasis,.hljs-quote{font-style:italic}.hljs-keyword,.hljs-params,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-selector-tag,.hljs-strong,.hljs-template-tag{font-weight:700}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#333;background:#fff}.hljs-comment,.hljs-quote{color:#777;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number{color:#777}.hljs-doctag,.hljs-formula,.hljs-string{color:#333;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAJ0lEQVQIW2O8e/fufwYGBgZBQUEQxcCIIfDu3Tuwivfv30NUoAsAALHpFMMLqZlPAAAAAElFTkSuQmCC)}.hljs-section,.hljs-selector-id,.hljs-title{color:#000;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-name,.hljs-title.class_,.hljs-type{color:#333;font-weight:700}.hljs-tag{color:#333}.hljs-regexp{color:#333;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAICAYAAADA+m62AAAAPUlEQVQYV2NkQAN37979r6yszIgujiIAU4RNMVwhuiQ6H6wQl3XI4oy4FMHcCJPHcDS6J2A2EqUQpJhohQDexSef15DBCwAAAABJRU5ErkJggg==)}.hljs-bullet,.hljs-link,.hljs-symbol{color:#000;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAKElEQVQIW2NkQAO7d+/+z4gsBhJwdXVlhAvCBECKwIIwAbhKZBUwBQA6hBpm5efZsgAAAABJRU5ErkJggg==)}.hljs-built_in{color:#000;text-decoration:underline}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{color:#fff;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAE0lEQVQIW2MMDQ39zzhz5kwIAQAyxweWgUHd1AAAAABJRU5ErkJggg==)}.hljs-addition{color:#000;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAALUlEQVQYV2N89+7dfwYk8P79ewZBQUFkIQZGOiu6e/cuiptQHAPl0NtNxAQBAM97Oejj3Dg7AAAAAElFTkSuQmCC)}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#1d1f21;color:#c5c8c6}.hljs span::selection,.hljs::selection{background:#373b41}.hljs span::-moz-selection,.hljs::-moz-selection{background:#373b41}.hljs-name,.hljs-title{color:#f0c674}.hljs-comment,.hljs-meta,.hljs-meta .hljs-keyword{color:#707880}.hljs-deletion,.hljs-link,.hljs-literal,.hljs-number,.hljs-symbol{color:#c66}.hljs-addition,.hljs-doctag,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-string{color:#b5bd68}.hljs-attribute,.hljs-code,.hljs-selector-id{color:#b294bb}.hljs-bullet,.hljs-keyword,.hljs-selector-tag,.hljs-tag{color:#81a2be}.hljs-subst,.hljs-template-tag,.hljs-template-variable,.hljs-variable{color:#8abeb7}.hljs-built_in,.hljs-quote,.hljs-section,.hljs-selector-class,.hljs-type{color:#de935f}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

Some files were not shown because too many files have changed in this diff Show More