ghostwriter-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.x"
17
+ - run: pip install -e ".[dev]"
18
+ - run: python -m pytest tests/ -v
19
+ - run: pip install build
20
+ - run: python -m build
21
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ config.json
3
+ *.pyc
4
+ dist/
5
+ *.egg-info/
6
+ .pytest_cache/
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2025-06-17)
4
+
5
+ - Initial PyPI release.
6
+ - Markdown → Ghost Lexical publishing.
7
+ - Ghost → WeChat draft sync with HTML processing pipeline.
8
+ - CLI: `list`, `publish`, sync, and `--preview` commands.
@@ -0,0 +1,113 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project overview
6
+
7
+ ghostwriter is a Python CLI that implements a **Markdown → Ghost → WeChat** publishing pipeline. Write Markdown, publish to a Ghost blog, and sync to a WeChat Official Account draft.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Install (from PyPI)
13
+ pip install ghostwriter-cli
14
+
15
+ # Install (editable, with dev deps)
16
+ pip install -e ".[dev]"
17
+
18
+ # Run tests
19
+ python -m pytest tests/ -v
20
+
21
+ # CLI
22
+ ghostwriter list # list Ghost posts
23
+ ghostwriter publish article.md # publish Markdown to Ghost
24
+ ghostwriter publish article.md --draft --wechat
25
+ ghostwriter <article-id> # sync Ghost→WeChat
26
+ ghostwriter --preview <article-id> # preview HTML, no draft
27
+ ghostwriter config # show config (secrets masked)
28
+ ghostwriter config set <key> <value> # write config key
29
+ ghostwriter config path # print config file path
30
+ ```
31
+
32
+ ## Configuration architecture
33
+
34
+ Two-tier system, env vars take priority:
35
+
36
+ 1. **Environment variables** (primary, CI/Docker):
37
+ `GHOSTWRITER_GHOST_API_URL`, `GHOSTWRITER_GHOST_ADMIN_KEY_ID`,
38
+ `GHOSTWRITER_GHOST_ADMIN_KEY`, `GHOSTWRITER_WECHAT_APPID`,
39
+ `GHOSTWRITER_WECHAT_SECRET`
40
+
41
+ 2. **Config file** (fallback): `~/.config/ghostwriter/config.json`,
42
+ managed via `ghostwriter config set <key> <value>`
43
+
44
+ `load_config()` validates all 5 required keys are present, exits with helpful
45
+ error if any are missing. See `src/ghostwriter/config.py`.
46
+
47
+ ## Package structure
48
+
49
+ ```
50
+ src/ghostwriter/
51
+ ├── __init__.py # public API re-exports
52
+ ├── __main__.py # python -m ghostwriter
53
+ ├── cli.py # CLI dispatch: main(), sync_article, publish_md_to_ghost, _cmd_publish
54
+ ├── config.py # load_config(), set_config_value(), show_config()
55
+ ├── ghost.py # Ghost Admin API: JWT auth, CRUD, image upload, author lookup
56
+ ├── wechat.py # WeChat API: token cache, material upload, draft creation
57
+ ├── cleaner.py # _WeChatCleaner HTML parser + whitelists + clean_html_for_wechat
58
+ ├── pipeline.py # process_html() + 14 transform stages (hr, table, links, lists, etc.)
59
+ ├── lexical.py # md_to_ghost_lexical(): Markdown → Ghost Lexical JSON
60
+ └── normalize.py # normalize_title(): Unicode → ASCII for WeChat compatibility
61
+ ```
62
+
63
+ ## Architecture
64
+
65
+ ### 1. Ghost API layer (`ghost.py`)
66
+ JWT auth (`get_ghost_token`) → generic `_api_request` helper used by
67
+ `ghost_api_get/post/put/delete`. All Admin API calls go through path
68
+ normalization: `{api_url.rstrip('/')}/{path.lstrip('/')}`.
69
+ `upload_image_to_ghost` and `get_ghost_authors` build their own URLs (Content API).
70
+
71
+ ### 2. WeChat API layer (`wechat.py`)
72
+ Token management with disk cache (`/tmp/wechat_token.json`), permanent
73
+ material upload for images, draft creation. Token is cached until 60s
74
+ before expiry.
75
+
76
+ ### 3. HTML processing pipeline (`pipeline.py`)
77
+ **Core of Ghost→WeChat conversion.** Order is strict — reordering breaks things:
78
+
79
+ 1. Replace image URLs with WeChat CDN URLs
80
+ 2. Strip Ghost HTML comments (`<!--kg-card-*-->`)
81
+ 3. Convert `<hr>` → styled `<div>` (before whitelist)
82
+ 4. Convert `<table>` → inline-block `<span>` layout (before whitelist)
83
+ 5. Extract code blocks to placeholders (protect from filter)
84
+ 6. Three-level whitelist filter: tags → attributes → CSS properties
85
+ 7. Restore code blocks with WeChat-safe `<pre>` styling
86
+ 8. Apply default styles (heading sizes, inline code, blockquote)
87
+ 9. Flatten nested blockquotes
88
+ 10. Paragraph/image spacing
89
+ 11. Convert links to `text [url]`
90
+ 12. Convert `<ol>` → numbered `<p>`, `<ul>` → bullet `<p>`
91
+
92
+ Whitelists live in `cleaner.py`: `WECHAT_SAFE_TAGS`, `WECHAT_SAFE_ATTRS`,
93
+ `WECHAT_SAFE_STYLES`.
94
+
95
+ ### 4. Markdown → Ghost Lexical (`lexical.py`)
96
+ Converts Markdown to Ghost Lexical JSON (not mobiledoc). Tables become
97
+ `html` cards. Supports headings, code blocks, lists, hr, inline formatting
98
+ (bold, italic, code, links).
99
+
100
+ ### 5. CLI (`cli.py`)
101
+ Commands: `list`, `publish`, `config`, sync (by article ID or `--preview`).
102
+ `main()` handles dispatch. `sync_article()` is the Ghost→WeChat flow.
103
+ `publish_md_to_ghost()` handles Markdown→Ghost with author lookup fallback.
104
+
105
+ ## Key design decisions
106
+
107
+ - **Code block protection:** Extracted to `__CODE_BLOCK_PLACEHOLDER__N__` before
108
+ the whitelist filter, restored afterward. Prevents filter from stripping tags.
109
+ - **Author fallback chain:** Ghost Content API → hardcoded IDs for known slugs.
110
+ - **WeChat constraints:** Author ≤8 bytes, digest ≤120 bytes, titles must not
111
+ contain Unicode special characters (normalized via `normalize_title()`).
112
+ - **Preview mode skips WeChat:** `--preview` only reads Ghost article and saves
113
+ HTML to `/tmp`, no WeChat credentials needed.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 yinguobing
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghostwriter-cli
3
+ Version: 0.1.0
4
+ Summary: Markdown → Ghost → WeChat publishing pipeline
5
+ Project-URL: Homepage, https://github.com/yinguobing/ghostwriter
6
+ Author: yinguobing
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.9
10
+ Requires-Dist: pyjwt>=2.0
11
+ Requires-Dist: requests>=2.25
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # ghostwriter
17
+
18
+ Markdown → Ghost → WeChat 发布管道。写一次,多处发布。
19
+
20
+ ```
21
+ markdown → Ghost 博客 → 微信公众号
22
+ ```
23
+
24
+ ## 功能
25
+
26
+ - **发布**:Markdown 文件直接发布到 Ghost 博客(Lexical 格式,编辑器可编辑)
27
+ - **同步**:已发布的 Ghost 文章同步到微信公众号草稿箱
28
+ - **全自动**:发布后一步同步到微信
29
+
30
+ ## 安装
31
+
32
+ ```bash
33
+ # 从 PyPI 安装(推荐)
34
+ pip install ghostwriter-cli
35
+
36
+ # 或从源码安装(开发模式)
37
+ git clone https://github.com/yinguobing/ghostwriter.git
38
+ cd ghostwriter
39
+ pip install -e ".[dev]"
40
+ ```
41
+
42
+ ## 配置
43
+
44
+ 支持两种配置方式(环境变量优先):
45
+
46
+ ### 方式 1:环境变量(推荐用于 CI/Docker)
47
+
48
+ ```bash
49
+ export GHOSTWRITER_GHOST_API_URL="https://yinguobing.com"
50
+ export GHOSTWRITER_GHOST_ADMIN_KEY_ID="your_key_id"
51
+ export GHOSTWRITER_GHOST_ADMIN_KEY="your_hex_secret"
52
+ export GHOSTWRITER_WECHAT_APPID="your_wechat_appid"
53
+ export GHOSTWRITER_WECHAT_SECRET="your_wechat_secret"
54
+ ```
55
+
56
+ ### 方式 2:配置文件(适合本地使用)
57
+
58
+ ```bash
59
+ # 交互式设置
60
+ ghostwriter config set ghost.api_url https://yinguobing.com
61
+ ghostwriter config set ghost.admin_key_id your_key_id
62
+ ghostwriter config set ghost.admin_key your_hex_secret
63
+ ghostwriter config set wechat.appid your_wechat_appid
64
+ ghostwriter config set wechat.secret your_wechat_secret
65
+
66
+ # 查看当前配置(密钥已脱敏)
67
+ ghostwriter config
68
+ ```
69
+
70
+ 配置文件保存在 `~/.config/ghostwriter/config.json`。
71
+
72
+ 可选字段:
73
+ ```bash
74
+ # 作者映射(Ghost Content API 不可用时的离线回退)
75
+ ghostwriter config set authors.<slug> <ghost_author_id>
76
+ ```
77
+
78
+ **Ghost Admin API Key 获取:**
79
+ Ghost 后台 → Settings → Advanced → Integrations → Add custom integration → 复制 `Admin API Key`(格式为 `key_id:hex_secret`,拆成两段填入)
80
+
81
+ **微信 AppID/Secret 获取:**
82
+ [微信公众平台](https://mp.weixin.qq.com) → 设置与开发 → 基本配置
83
+
84
+ ## 用法
85
+
86
+ ### 列出 Ghost 文章
87
+
88
+ ```bash
89
+ ghostwriter list
90
+ ```
91
+
92
+ ### 发布 Markdown 到 Ghost
93
+
94
+ ```bash
95
+ # 直接发布
96
+ ghostwriter publish article.md
97
+
98
+ # 指定标题和标签
99
+ ghostwriter publish article.md --title "我的文章" --tags Ghost,开源
100
+
101
+ # 指定作者(slug,默认 xiaohei)
102
+ ghostwriter publish article.md --author guobing
103
+
104
+ # 先存草稿
105
+ ghostwriter publish article.md --draft
106
+
107
+ # 发布后自动同步到微信
108
+ ghostwriter publish article.md --wechat
109
+ ```
110
+
111
+ ### 同步 Ghost 文章到微信草稿
112
+
113
+ ```bash
114
+ # 先列出文章获取 ID
115
+ ghostwriter list
116
+
117
+ # 同步指定文章
118
+ ghostwriter <article-id>
119
+
120
+ # 预览 HTML(不创建草稿)
121
+ ghostwriter --preview <article-id>
122
+ ```
123
+
124
+ ## 管道说明
125
+
126
+ ### Markdown → Ghost(`publish` 命令)
127
+
128
+ 将 Markdown 文件转换为 Ghost 的 **Lexical 格式**(基于 `@tryghost/kg-lexical-html-renderer`),支持:
129
+
130
+ - 标题(h1~h6)
131
+ - 段落、粗体、斜体、行内代码、链接
132
+ - 围栏代码块(带语言标记)
133
+ - 表格(以 HTML card 渲染)
134
+ - 有序/无序列表
135
+ - 分割线
136
+
137
+ 转换后的文章在 Ghost 编辑器里可以正常编辑。
138
+
139
+ ### Ghost → 微信(`sync` 命令)
140
+
141
+ 从 Ghost API 获取文章 HTML,经过多层处理管道后推送到微信公众号草稿箱:
142
+
143
+ 1. 图片上传到微信永久素材,替换为 CDN 地址
144
+ 2. 白名单三层过滤(标签/属性/样式)
145
+ 3. 代码块保护与恢复
146
+ 4. 微信不支持的标签转换(table → div, ol/ul → 前缀段落, hr → 分隔线)
147
+ 5. 默认样式补全
148
+
149
+ ## 项目结构
150
+
151
+ ```
152
+ ghostwriter/
153
+ ├── pyproject.toml # 项目元数据、构建配置
154
+ ├── src/ghostwriter/ # 源码包
155
+ │ ├── cli.py # CLI 入口与命令分发
156
+ │ ├── config.py # 配置文件加载
157
+ │ ├── ghost.py # Ghost Admin API 客户端
158
+ │ ├── wechat.py # 微信公众号 API 客户端
159
+ │ ├── cleaner.py # HTML 白名单过滤器
160
+ │ ├── pipeline.py # Ghost → 微信 HTML 处理管道
161
+ │ ├── lexical.py # Markdown → Ghost Lexical 转换器
162
+ │ └── normalize.py # Unicode 标题规范化
163
+ ├── tests/ # 单元测试(pytest,74个)
164
+ └── docs/ # 项目页面
165
+ ```
166
+
167
+ ## 注意事项
168
+
169
+ - 微信草稿标题限制:Unicode 特殊字符(弯引号、破折号等)会触发 45003 错误,脚本会自动处理
170
+ - 作者字段在微信公众号中限制 8 字节
171
+ - 代码块使用 `<pre>` + 语言标签的样式方案,微信中可用
172
+ - `--wechat` 参数需要在 Ghost API 中能通过 slug 查到刚发布的文章
@@ -0,0 +1,157 @@
1
+ # ghostwriter
2
+
3
+ Markdown → Ghost → WeChat 发布管道。写一次,多处发布。
4
+
5
+ ```
6
+ markdown → Ghost 博客 → 微信公众号
7
+ ```
8
+
9
+ ## 功能
10
+
11
+ - **发布**:Markdown 文件直接发布到 Ghost 博客(Lexical 格式,编辑器可编辑)
12
+ - **同步**:已发布的 Ghost 文章同步到微信公众号草稿箱
13
+ - **全自动**:发布后一步同步到微信
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ # 从 PyPI 安装(推荐)
19
+ pip install ghostwriter-cli
20
+
21
+ # 或从源码安装(开发模式)
22
+ git clone https://github.com/yinguobing/ghostwriter.git
23
+ cd ghostwriter
24
+ pip install -e ".[dev]"
25
+ ```
26
+
27
+ ## 配置
28
+
29
+ 支持两种配置方式(环境变量优先):
30
+
31
+ ### 方式 1:环境变量(推荐用于 CI/Docker)
32
+
33
+ ```bash
34
+ export GHOSTWRITER_GHOST_API_URL="https://yinguobing.com"
35
+ export GHOSTWRITER_GHOST_ADMIN_KEY_ID="your_key_id"
36
+ export GHOSTWRITER_GHOST_ADMIN_KEY="your_hex_secret"
37
+ export GHOSTWRITER_WECHAT_APPID="your_wechat_appid"
38
+ export GHOSTWRITER_WECHAT_SECRET="your_wechat_secret"
39
+ ```
40
+
41
+ ### 方式 2:配置文件(适合本地使用)
42
+
43
+ ```bash
44
+ # 交互式设置
45
+ ghostwriter config set ghost.api_url https://yinguobing.com
46
+ ghostwriter config set ghost.admin_key_id your_key_id
47
+ ghostwriter config set ghost.admin_key your_hex_secret
48
+ ghostwriter config set wechat.appid your_wechat_appid
49
+ ghostwriter config set wechat.secret your_wechat_secret
50
+
51
+ # 查看当前配置(密钥已脱敏)
52
+ ghostwriter config
53
+ ```
54
+
55
+ 配置文件保存在 `~/.config/ghostwriter/config.json`。
56
+
57
+ 可选字段:
58
+ ```bash
59
+ # 作者映射(Ghost Content API 不可用时的离线回退)
60
+ ghostwriter config set authors.<slug> <ghost_author_id>
61
+ ```
62
+
63
+ **Ghost Admin API Key 获取:**
64
+ Ghost 后台 → Settings → Advanced → Integrations → Add custom integration → 复制 `Admin API Key`(格式为 `key_id:hex_secret`,拆成两段填入)
65
+
66
+ **微信 AppID/Secret 获取:**
67
+ [微信公众平台](https://mp.weixin.qq.com) → 设置与开发 → 基本配置
68
+
69
+ ## 用法
70
+
71
+ ### 列出 Ghost 文章
72
+
73
+ ```bash
74
+ ghostwriter list
75
+ ```
76
+
77
+ ### 发布 Markdown 到 Ghost
78
+
79
+ ```bash
80
+ # 直接发布
81
+ ghostwriter publish article.md
82
+
83
+ # 指定标题和标签
84
+ ghostwriter publish article.md --title "我的文章" --tags Ghost,开源
85
+
86
+ # 指定作者(slug,默认 xiaohei)
87
+ ghostwriter publish article.md --author guobing
88
+
89
+ # 先存草稿
90
+ ghostwriter publish article.md --draft
91
+
92
+ # 发布后自动同步到微信
93
+ ghostwriter publish article.md --wechat
94
+ ```
95
+
96
+ ### 同步 Ghost 文章到微信草稿
97
+
98
+ ```bash
99
+ # 先列出文章获取 ID
100
+ ghostwriter list
101
+
102
+ # 同步指定文章
103
+ ghostwriter <article-id>
104
+
105
+ # 预览 HTML(不创建草稿)
106
+ ghostwriter --preview <article-id>
107
+ ```
108
+
109
+ ## 管道说明
110
+
111
+ ### Markdown → Ghost(`publish` 命令)
112
+
113
+ 将 Markdown 文件转换为 Ghost 的 **Lexical 格式**(基于 `@tryghost/kg-lexical-html-renderer`),支持:
114
+
115
+ - 标题(h1~h6)
116
+ - 段落、粗体、斜体、行内代码、链接
117
+ - 围栏代码块(带语言标记)
118
+ - 表格(以 HTML card 渲染)
119
+ - 有序/无序列表
120
+ - 分割线
121
+
122
+ 转换后的文章在 Ghost 编辑器里可以正常编辑。
123
+
124
+ ### Ghost → 微信(`sync` 命令)
125
+
126
+ 从 Ghost API 获取文章 HTML,经过多层处理管道后推送到微信公众号草稿箱:
127
+
128
+ 1. 图片上传到微信永久素材,替换为 CDN 地址
129
+ 2. 白名单三层过滤(标签/属性/样式)
130
+ 3. 代码块保护与恢复
131
+ 4. 微信不支持的标签转换(table → div, ol/ul → 前缀段落, hr → 分隔线)
132
+ 5. 默认样式补全
133
+
134
+ ## 项目结构
135
+
136
+ ```
137
+ ghostwriter/
138
+ ├── pyproject.toml # 项目元数据、构建配置
139
+ ├── src/ghostwriter/ # 源码包
140
+ │ ├── cli.py # CLI 入口与命令分发
141
+ │ ├── config.py # 配置文件加载
142
+ │ ├── ghost.py # Ghost Admin API 客户端
143
+ │ ├── wechat.py # 微信公众号 API 客户端
144
+ │ ├── cleaner.py # HTML 白名单过滤器
145
+ │ ├── pipeline.py # Ghost → 微信 HTML 处理管道
146
+ │ ├── lexical.py # Markdown → Ghost Lexical 转换器
147
+ │ └── normalize.py # Unicode 标题规范化
148
+ ├── tests/ # 单元测试(pytest,74个)
149
+ └── docs/ # 项目页面
150
+ ```
151
+
152
+ ## 注意事项
153
+
154
+ - 微信草稿标题限制:Unicode 特殊字符(弯引号、破折号等)会触发 45003 错误,脚本会自动处理
155
+ - 作者字段在微信公众号中限制 8 字节
156
+ - 代码块使用 `<pre>` + 语言标签的样式方案,微信中可用
157
+ - `--wechat` 参数需要在 Ghost API 中能通过 slug 查到刚发布的文章