markdown-docx 0.1.0__tar.gz → 0.2.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.
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/.github/workflows/ci.yml +4 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/.github/workflows/publish-pypi.yml +4 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/CHANGELOG.md +7 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/PKG-INFO +37 -3
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/README.md +35 -2
- markdown_docx-0.2.0/docs/skill-management.md +69 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/pyproject.toml +2 -1
- markdown_docx-0.2.0/scripts/smoke_skill.py +77 -0
- markdown_docx-0.2.0/src/markdown_docx/__init__.py +1 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/assets/syntax.json +2 -2
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/cli.py +45 -13
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/markdown_body.py +1 -1
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/parser.py +2 -2
- markdown_docx-0.2.0/src/markdown_docx/skill.py +421 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/conftest.py +9 -0
- markdown_docx-0.2.0/tests/test_cli.py +298 -0
- markdown_docx-0.2.0/tests/test_skill.py +487 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/uv.lock +3 -1
- markdown_docx-0.1.0/src/markdown_docx/__init__.py +0 -1
- markdown_docx-0.1.0/src/markdown_docx/skill.py +0 -123
- markdown_docx-0.1.0/tests/test_cli.py +0 -163
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/.gitignore +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/AGENTS.md +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/LICENSE +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/PLAN.md +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/docs/public-api-capabilities.md +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-lunch-chase.png +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-run-finish.png +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-run-start.png +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-trio-cameo.png +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-trio-inline.png +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-wagon-rescue.png +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/illustration-prompts.md +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/word-icon.png +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/word-workflow.png +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/showcase.docx +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/showcase.md +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/scripts/Export-DocxPdf.ps1 +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/scripts/build_default_template.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/scripts/build_qa_templates.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/scripts/build_showcase_assets.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/assets/default.docx +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/assets.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/errors.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/images.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/metadata.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/models.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/renderer.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/styles.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/template.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_metadata.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_parser.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_public_api_boundary.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_public_api_capabilities.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_renderer_images.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_renderer_lists.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_renderer_sections.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_renderer_tables.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_renderer_text.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_showcase.py +0 -0
- {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_template.py +0 -0
|
@@ -60,6 +60,9 @@ jobs:
|
|
|
60
60
|
- name: Run tests
|
|
61
61
|
run: uv run pytest
|
|
62
62
|
|
|
63
|
+
- name: Smoke-test editable skill lifecycle
|
|
64
|
+
run: uv run python scripts/smoke_skill.py --expected-source local
|
|
65
|
+
|
|
63
66
|
visual-smoke:
|
|
64
67
|
runs-on: ubuntu-latest
|
|
65
68
|
steps:
|
|
@@ -115,6 +118,7 @@ jobs:
|
|
|
115
118
|
.wheel-smoke/bin/python -m pip install dist/*.whl
|
|
116
119
|
.wheel-smoke/bin/markdown-docx --version
|
|
117
120
|
.wheel-smoke/bin/markdown-docx --syntax --json
|
|
121
|
+
.wheel-smoke/bin/python scripts/smoke_skill.py
|
|
118
122
|
echo '# Wheel smoke test' > smoke.md
|
|
119
123
|
.wheel-smoke/bin/markdown-docx smoke.md smoke.docx
|
|
120
124
|
|
|
@@ -42,6 +42,9 @@ jobs:
|
|
|
42
42
|
- name: Run tests
|
|
43
43
|
run: uv run pytest
|
|
44
44
|
|
|
45
|
+
- name: Smoke-test editable skill lifecycle
|
|
46
|
+
run: uv run python scripts/smoke_skill.py --expected-source local
|
|
47
|
+
|
|
45
48
|
- name: Verify release tag matches package version
|
|
46
49
|
if: github.event_name == 'release' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
|
|
47
50
|
shell: bash
|
|
@@ -83,6 +86,7 @@ jobs:
|
|
|
83
86
|
.wheel-smoke/bin/python -m pip install dist/*.whl
|
|
84
87
|
.wheel-smoke/bin/markdown-docx --version
|
|
85
88
|
.wheel-smoke/bin/markdown-docx --inspect-template --json
|
|
89
|
+
.wheel-smoke/bin/python scripts/smoke_skill.py
|
|
86
90
|
|
|
87
91
|
- name: Upload distributions
|
|
88
92
|
uses: actions/upload-artifact@v7.0.1
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Synchronize existing pristine managed skills to the running CLI version during normal commands.
|
|
6
|
+
- Store managed ownership, version, and normalized content hashes in `SKILL.md` front matter. Migrate legacy managed skills and recover invalid version metadata.
|
|
7
|
+
- Add read-only skill status and force installation for managed edits. Preserve custom directory support, removal safety, and JSON output.
|
|
8
|
+
- Skip automatic synchronization for local source and editable builds. Add atomic replacement, concurrent-change checks, and installed-wheel lifecycle smoke tests.
|
|
9
|
+
|
|
3
10
|
## 0.1.0
|
|
4
11
|
|
|
5
12
|
- Add strict Markdown parsing with invisible YAML directives and line-aware diagnostics.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: markdown-docx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Convert constrained Markdown documents into editable Word files.
|
|
5
5
|
Project-URL: Homepage, https://github.com/pseudosavant/markdown-docx
|
|
6
6
|
Project-URL: Repository, https://github.com/pseudosavant/markdown-docx
|
|
@@ -24,6 +24,7 @@ Classifier: Topic :: Text Processing :: Markup
|
|
|
24
24
|
Requires-Python: >=3.11
|
|
25
25
|
Requires-Dist: httpx<1,>=0.27
|
|
26
26
|
Requires-Dist: markdown-it-py<5,>=3.0
|
|
27
|
+
Requires-Dist: packaging>=24.0
|
|
27
28
|
Requires-Dist: pillow<13,>=10.0
|
|
28
29
|
Requires-Dist: python-docx==1.2.0
|
|
29
30
|
Requires-Dist: pyyaml<7,>=6.0
|
|
@@ -53,6 +54,39 @@ Then ask an agent to create both the Markdown source and the editable Word outpu
|
|
|
53
54
|
|
|
54
55
|
The skill teaches the agent how to inspect the format, inspect blank Word templates, render safely, and handle structured results.
|
|
55
56
|
|
|
57
|
+
## Manage the agent skill
|
|
58
|
+
|
|
59
|
+
The standard location is `~/.agents/skills/markdown-docx/SKILL.md`. Normally installed CLI builds automatically synchronize an already-installed managed skill during ordinary commands, including help and version output. The running CLI version is the authority. A pristine older skill is replaced with the bundled skill. Equal and newer versions are left alone. Missing and unmanaged skills are never installed or overwritten automatically.
|
|
60
|
+
|
|
61
|
+
Synchronization is local. It does not query a package index, refresh uv's cache, or update the CLI. The skill continues to instruct agents to use `uvx markdown-docx`. Updates affect future skill loading. Instructions already loaded by a running agent may stay unchanged until the agent reloads them.
|
|
62
|
+
|
|
63
|
+
Inspect the skill without changing it:
|
|
64
|
+
|
|
65
|
+
```powershell
|
|
66
|
+
uvx markdown-docx skill status
|
|
67
|
+
uvx markdown-docx skill status --json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Status reports the selected path, ownership, CLI and skill versions, version ordering, integrity, and automatic synchronization eligibility. Modified or unverifiable skills with valid version metadata are preserved. To replace a managed skill intentionally:
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
uvx markdown-docx skill install --force
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Install `--force` still refuses unmanaged content and never downgrades a newer skill. A normal install creates a missing skill, updates a pristine older skill, or leaves a current skill alone. Legacy managed skills and managed skills with missing or invalid version metadata receive a fresh replacement. This recovery does not require a valid hash.
|
|
77
|
+
|
|
78
|
+
Custom directories require explicit updates. Automatic synchronization only checks the standard directory. Local source and editable development builds skip automatic synchronization, but explicit installation still works:
|
|
79
|
+
|
|
80
|
+
```powershell
|
|
81
|
+
uvx markdown-docx skill install --skills-dir PATH
|
|
82
|
+
uvx markdown-docx skill status --skills-dir PATH
|
|
83
|
+
uvx --from . markdown-docx skill install
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Remove the skill with `uvx markdown-docx skill remove`. Removal refuses unmanaged content and extra files unless `--force` is supplied. All skill commands accept `--skills-dir` and `--json`. Skill commands never trigger automatic synchronization. Maintenance notices go to stderr and leave JSON stdout intact.
|
|
87
|
+
|
|
88
|
+
See [skill lifecycle metadata and decisions](docs/skill-management.md) for the hash format and recovery rules.
|
|
89
|
+
|
|
56
90
|
## Use the CLI directly
|
|
57
91
|
|
|
58
92
|
```powershell
|
|
@@ -74,7 +108,7 @@ uvx markdown-docx --syntax --json
|
|
|
74
108
|
|
|
75
109
|
## Supported Markdown
|
|
76
110
|
|
|
77
|
-
Version 0.
|
|
111
|
+
Version 0.2.0 supports:
|
|
78
112
|
|
|
79
113
|
- ATX headings from `#` through `######`
|
|
80
114
|
- Paragraphs and standard soft or hard line breaks
|
|
@@ -86,7 +120,7 @@ Version 0.1.0 supports:
|
|
|
86
120
|
- Local and remote inline images
|
|
87
121
|
- Standalone images with width and alignment metadata
|
|
88
122
|
|
|
89
|
-
Links are rejected in 0.
|
|
123
|
+
Links are rejected in 0.2.0. `python-docx` 1.2.0 can read hyperlinks but has no supported public API for creating them. The source alt text for images remains meaningful Markdown content, but the same library release has no public API for embedding it in a Word drawing. A rendered document containing images reports `image_alt_text_not_embedded` in its warning list.
|
|
90
124
|
|
|
91
125
|
The following syntax is intentionally unsupported:
|
|
92
126
|
|
|
@@ -22,6 +22,39 @@ Then ask an agent to create both the Markdown source and the editable Word outpu
|
|
|
22
22
|
|
|
23
23
|
The skill teaches the agent how to inspect the format, inspect blank Word templates, render safely, and handle structured results.
|
|
24
24
|
|
|
25
|
+
## Manage the agent skill
|
|
26
|
+
|
|
27
|
+
The standard location is `~/.agents/skills/markdown-docx/SKILL.md`. Normally installed CLI builds automatically synchronize an already-installed managed skill during ordinary commands, including help and version output. The running CLI version is the authority. A pristine older skill is replaced with the bundled skill. Equal and newer versions are left alone. Missing and unmanaged skills are never installed or overwritten automatically.
|
|
28
|
+
|
|
29
|
+
Synchronization is local. It does not query a package index, refresh uv's cache, or update the CLI. The skill continues to instruct agents to use `uvx markdown-docx`. Updates affect future skill loading. Instructions already loaded by a running agent may stay unchanged until the agent reloads them.
|
|
30
|
+
|
|
31
|
+
Inspect the skill without changing it:
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
uvx markdown-docx skill status
|
|
35
|
+
uvx markdown-docx skill status --json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Status reports the selected path, ownership, CLI and skill versions, version ordering, integrity, and automatic synchronization eligibility. Modified or unverifiable skills with valid version metadata are preserved. To replace a managed skill intentionally:
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
uvx markdown-docx skill install --force
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Install `--force` still refuses unmanaged content and never downgrades a newer skill. A normal install creates a missing skill, updates a pristine older skill, or leaves a current skill alone. Legacy managed skills and managed skills with missing or invalid version metadata receive a fresh replacement. This recovery does not require a valid hash.
|
|
45
|
+
|
|
46
|
+
Custom directories require explicit updates. Automatic synchronization only checks the standard directory. Local source and editable development builds skip automatic synchronization, but explicit installation still works:
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
uvx markdown-docx skill install --skills-dir PATH
|
|
50
|
+
uvx markdown-docx skill status --skills-dir PATH
|
|
51
|
+
uvx --from . markdown-docx skill install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Remove the skill with `uvx markdown-docx skill remove`. Removal refuses unmanaged content and extra files unless `--force` is supplied. All skill commands accept `--skills-dir` and `--json`. Skill commands never trigger automatic synchronization. Maintenance notices go to stderr and leave JSON stdout intact.
|
|
55
|
+
|
|
56
|
+
See [skill lifecycle metadata and decisions](docs/skill-management.md) for the hash format and recovery rules.
|
|
57
|
+
|
|
25
58
|
## Use the CLI directly
|
|
26
59
|
|
|
27
60
|
```powershell
|
|
@@ -43,7 +76,7 @@ uvx markdown-docx --syntax --json
|
|
|
43
76
|
|
|
44
77
|
## Supported Markdown
|
|
45
78
|
|
|
46
|
-
Version 0.
|
|
79
|
+
Version 0.2.0 supports:
|
|
47
80
|
|
|
48
81
|
- ATX headings from `#` through `######`
|
|
49
82
|
- Paragraphs and standard soft or hard line breaks
|
|
@@ -55,7 +88,7 @@ Version 0.1.0 supports:
|
|
|
55
88
|
- Local and remote inline images
|
|
56
89
|
- Standalone images with width and alignment metadata
|
|
57
90
|
|
|
58
|
-
Links are rejected in 0.
|
|
91
|
+
Links are rejected in 0.2.0. `python-docx` 1.2.0 can read hyperlinks but has no supported public API for creating them. The source alt text for images remains meaningful Markdown content, but the same library release has no public API for embedding it in a Word drawing. A rendered document containing images reports `image_alt_text_not_embedded` in its warning list.
|
|
59
92
|
|
|
60
93
|
The following syntax is intentionally unsupported:
|
|
61
94
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Managed agent skill
|
|
2
|
+
|
|
3
|
+
The distribution, CLI command, and skill are named `markdown-docx`. The Python import package is `markdown_docx`. `markdown_docx.__version__` supplies the version reported by the CLI and written by the canonical skill renderer in `src/markdown_docx/skill.py`.
|
|
4
|
+
|
|
5
|
+
## Metadata
|
|
6
|
+
|
|
7
|
+
New installations write lifecycle metadata inside the existing YAML front matter:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
---
|
|
11
|
+
name: markdown-docx
|
|
12
|
+
description: Existing skill description
|
|
13
|
+
metadata:
|
|
14
|
+
managed-by: markdown-docx
|
|
15
|
+
managed-version: "0.2.0"
|
|
16
|
+
managed-content-sha256: "sha256:<64 lowercase hexadecimal characters>"
|
|
17
|
+
---
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The description and all instructions come from one bundled template. The version is always a quoted string with the exact running CLI version. There is no top-level version field or sidecar file. New files use UTF-8 without a BOM, LF line endings, and a trailing newline.
|
|
21
|
+
|
|
22
|
+
To calculate the hash, render the complete file with `managed-content-sha256: ""`, normalize CRLF and CR to LF, then calculate SHA-256 over the UTF-8 bytes. Replace only the hash value with `sha256:` followed by the digest. Verification blanks that value using parsed YAML source positions. It preserves the rest of the original text rather than serializing YAML again. The hash covers the instructions, examples, front matter, and formatting. It detects edits and is not a signature or security boundary.
|
|
23
|
+
|
|
24
|
+
Front matter ownership is authoritative. The old `<!-- managed-by: markdown-docx -->` marker still identifies legacy managed files when no conflicting `managed-by` field is present. New files omit that marker. Unrelated metadata in the bundled template remains part of the generated skill and its hash.
|
|
25
|
+
|
|
26
|
+
## Automatic decisions
|
|
27
|
+
|
|
28
|
+
All normal CLI invocations check `~/.agents/skills/markdown-docx/SKILL.md`, including rendering, inspection, help, version, about, and no-argument help. Skill commands do not run this check.
|
|
29
|
+
|
|
30
|
+
| Installed state | Automatic action |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| Missing directory or file | Leave absent |
|
|
33
|
+
| Unmanaged or owned by another tool | Leave untouched |
|
|
34
|
+
| Legacy managed file without a version | Treat as version 0 and replace |
|
|
35
|
+
| Managed file with missing or invalid version | Replace without hash verification |
|
|
36
|
+
| Equal or newer valid version | Leave untouched |
|
|
37
|
+
| Older version with matching stored hash | Replace with the running CLI's bundled skill |
|
|
38
|
+
| Older version with missing, malformed, or mismatched hash | Preserve and recommend force installation |
|
|
39
|
+
|
|
40
|
+
Versions use PEP 440 ordering through `packaging.version.Version`. An invalid running CLI version disables automatic synchronization. Verification compares the installed file against its own stored hash. It never compares an older file against the current bundled hash to decide whether the older file was edited.
|
|
41
|
+
|
|
42
|
+
Replacements use a flushed and closed temporary file in the skill directory followed by an atomic replacement. The installed file is read again immediately before replacement. Any observed concurrent change cancels the replacement. There is no lock or long retry loop. An external writer can still race the final filesystem operation.
|
|
43
|
+
|
|
44
|
+
Updates and preservation notices go to stderr. Failures are best effort and do not change the primary command's exit status or JSON stdout. No notice is emitted for absent, unmanaged, current, or newer skills, or skipped development builds.
|
|
45
|
+
|
|
46
|
+
## Explicit commands
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
uvx markdown-docx skill install
|
|
50
|
+
uvx markdown-docx skill install --force
|
|
51
|
+
uvx markdown-docx skill status --json
|
|
52
|
+
uvx markdown-docx skill remove
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A normal install creates a missing skill and updates a pristine older skill. It refuses altered or unverifiable managed files with valid versions, including edits to the current version. Use `uvx markdown-docx skill install --force` to restore such a file. Force installation still refuses unmanaged content and never replaces a newer version. Missing or invalid managed versions use the recovery rule in the table.
|
|
56
|
+
|
|
57
|
+
Status is read-only. Plain and JSON results include the selected path, standard-location flag, installation and ownership state, exact CLI version, valid installed version, version relation, integrity state, runtime source, automatic synchronization eligibility and reason, and a force command when applicable. Missing or malformed managed versions are reported as null with a separate version state. Legacy status reports legacy integrity and compares its effective version 0 before migration.
|
|
58
|
+
|
|
59
|
+
Install retains the existing `installed`, `created`, `updated`, `skill`, and `path` JSON fields. Removal retains its existing result fields and force semantics. It refuses unmanaged skills or extra directory entries unless removal `--force` is supplied. Legacy managed skills remain removable. Installation and synchronization only write `SKILL.md`. They preserve unrelated files. Existing directories without `SKILL.md` are refused by explicit install and removal.
|
|
60
|
+
|
|
61
|
+
All three commands accept `--skills-dir PATH`. Custom directories participate only in explicit commands. Include the same directory when updating or forcing a custom installation.
|
|
62
|
+
|
|
63
|
+
## Runtime source and loading
|
|
64
|
+
|
|
65
|
+
Automatic synchronization uses installed-distribution records and PEP 610 `direct_url.json`. Local directories, local source archives, and editable installations are excluded. Source code that does not match the installed distribution's module path is also excluded. Unidentified or malformed provenance is handled conservatively. An installed wheel, including one installed from a local wheel file, remains eligible. No launcher detection is used.
|
|
66
|
+
|
|
67
|
+
Explicit installation remains available from a checkout with `uvx --from . markdown-docx skill install`. Tests redirect the standard skills directory to temporary storage. The package smoke check verifies wheel provenance, runtime version, canonical content, and lifecycle behavior in an isolated temporary skill directory.
|
|
68
|
+
|
|
69
|
+
This feature only synchronizes local skill content to the CLI that is already running. It never queries PyPI, updates the CLI, or refreshes uv's cache. Agent instructions continue to invoke `uvx markdown-docx`. Updates apply to future skill loading and may not affect instructions already loaded in an active agent session.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "markdown-docx"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Convert constrained Markdown documents into editable Word files."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -28,6 +28,7 @@ classifiers = [
|
|
|
28
28
|
dependencies = [
|
|
29
29
|
"httpx>=0.27,<1",
|
|
30
30
|
"markdown-it-py>=3.0,<5",
|
|
31
|
+
"packaging>=24.0",
|
|
31
32
|
"Pillow>=10.0,<13",
|
|
32
33
|
"PyYAML>=6.0,<7",
|
|
33
34
|
"python-docx==1.2.0",
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Check packaged skill behavior without accessing the user's skills directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import tempfile
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
|
|
13
|
+
from markdown_docx import __version__, skill
|
|
14
|
+
from markdown_docx.cli import main
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def invoke(args: list[str]) -> tuple[str, str]:
|
|
18
|
+
stdout, stderr = io.StringIO(), io.StringIO()
|
|
19
|
+
code = main(args, stdout=stdout, stderr=stderr)
|
|
20
|
+
assert code == 0, (code, stdout.getvalue(), stderr.getvalue())
|
|
21
|
+
return stdout.getvalue(), stderr.getvalue()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def check(expected_source: str) -> None:
|
|
25
|
+
assert skill.runtime_source() == expected_source
|
|
26
|
+
assert version("markdown-docx") == __version__
|
|
27
|
+
with tempfile.TemporaryDirectory(prefix="markdown-docx-skill-smoke-") as directory:
|
|
28
|
+
root = Path(directory) / "skills"
|
|
29
|
+
with patch.object(skill, "default_skills_dir", return_value=root):
|
|
30
|
+
stdout, stderr = invoke(["--version"])
|
|
31
|
+
assert stdout == f"markdown-docx {__version__}\n" and not stderr
|
|
32
|
+
assert not root.exists()
|
|
33
|
+
stdout, stderr = invoke(["skill", "install", "--json"])
|
|
34
|
+
installed = json.loads(stdout)
|
|
35
|
+
assert installed["created"] and not stderr
|
|
36
|
+
path = Path(installed["path"])
|
|
37
|
+
canonical = path.read_bytes()
|
|
38
|
+
assert canonical == skill.render_skill().encode("utf-8")
|
|
39
|
+
assert b"Always invoke the tool as `uvx markdown-docx ...`" in canonical
|
|
40
|
+
assert list(path.parent.iterdir()) == [path]
|
|
41
|
+
stdout, stderr = invoke(["skill", "status", "--json"])
|
|
42
|
+
status = json.loads(stdout)
|
|
43
|
+
assert status["managed_version"] == __version__
|
|
44
|
+
assert status["integrity"] == "valid" and not stderr
|
|
45
|
+
assert status["local_development"] == (expected_source == "local")
|
|
46
|
+
older = skill.render_skill("0.0.0").encode("utf-8")
|
|
47
|
+
path.write_bytes(older)
|
|
48
|
+
stdout, stderr = invoke(["--syntax", "--json"])
|
|
49
|
+
assert json.loads(stdout)["ok"]
|
|
50
|
+
if expected_source == "installed":
|
|
51
|
+
assert "Updated managed skill" in stderr
|
|
52
|
+
assert path.read_bytes() == canonical
|
|
53
|
+
else:
|
|
54
|
+
assert not stderr
|
|
55
|
+
assert path.read_bytes() == older
|
|
56
|
+
altered = older + b"Local edits\n"
|
|
57
|
+
path.write_bytes(altered)
|
|
58
|
+
stdout, stderr = invoke(["--syntax", "--json"])
|
|
59
|
+
assert json.loads(stdout)["ok"]
|
|
60
|
+
assert path.read_bytes() == altered
|
|
61
|
+
if expected_source == "installed":
|
|
62
|
+
assert skill.FORCE_INSTALL_COMMAND in stderr
|
|
63
|
+
else:
|
|
64
|
+
assert not stderr
|
|
65
|
+
stdout, stderr = invoke(["skill", "install", "--force", "--json"])
|
|
66
|
+
assert json.loads(stdout)["updated"] and not stderr
|
|
67
|
+
assert path.read_bytes() == canonical
|
|
68
|
+
stdout, stderr = invoke(["skill", "remove", "--json"])
|
|
69
|
+
assert json.loads(stdout)["removed"] and not stderr
|
|
70
|
+
assert not path.exists()
|
|
71
|
+
print(f"Managed skill smoke passed for {expected_source} runtime {__version__}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
76
|
+
parser.add_argument("--expected-source", choices=("installed", "local"), default="installed")
|
|
77
|
+
check(parser.parse_args().expected_source)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": "markdown-docx",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"text": "markdown-docx 0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"text": "markdown-docx 0.2.0 syntax\n\nSupported Markdown:\n ATX headings, paragraphs, emphasis, strong text, inline code, hard line breaks, fenced code blocks, blockquotes, ordered and unordered lists, pipe tables, and images.\n\nMetadata comments:\n document metadata must be first\n section metadata starts a next-page section\n <!-- markdown-docx: page-break --> inserts a page break\n table metadata must immediately precede a pipe table\n image metadata must immediately precede a standalone image\n\nLengths use in, cm, mm, or pt. Image widths may also use percentages.\n\nUnsupported in 0.2.0:\n links, raw HTML, task lists, footnotes, horizontal rules, indented code blocks, multi-paragraph list items, DOTX, floating images, headers and footers from Markdown, page-number fields, and direct OOXML features.",
|
|
5
5
|
"page_sizes": ["letter", "legal", "a4", "custom"],
|
|
6
6
|
"orientations": ["portrait", "landscape"],
|
|
7
7
|
"length_units": ["in", "cm", "mm", "pt"],
|
|
@@ -12,7 +12,7 @@ from markdown_docx.assets import load_syntax_payload
|
|
|
12
12
|
from markdown_docx.errors import EXIT_INTERNAL, InputError, MarkdownDocxError, UsageError
|
|
13
13
|
from markdown_docx.parser import parse_document
|
|
14
14
|
from markdown_docx.renderer import render_docx
|
|
15
|
-
from markdown_docx.skill import install_skill, remove_skill
|
|
15
|
+
from markdown_docx.skill import install_skill, remove_skill, skill_status, synchronize_skill
|
|
16
16
|
from markdown_docx.template import inspect_template
|
|
17
17
|
|
|
18
18
|
PROGRAM_NAME = "markdown-docx"
|
|
@@ -77,8 +77,10 @@ Inspection:
|
|
|
77
77
|
{PROGRAM_NAME} --list-table-styles [--template formatting.docx] [--json]
|
|
78
78
|
|
|
79
79
|
Agent skill:
|
|
80
|
-
{PROGRAM_NAME} skill install [--skills-dir DIR] [--json]
|
|
80
|
+
{PROGRAM_NAME} skill install [--skills-dir DIR] [--force] [--json]
|
|
81
81
|
{PROGRAM_NAME} skill remove [--skills-dir DIR] [--force] [--json]
|
|
82
|
+
{PROGRAM_NAME} skill status [--skills-dir DIR] [--json]
|
|
83
|
+
Installed managed skills sync locally during normal commands. See 'skill --help'.
|
|
82
84
|
|
|
83
85
|
Common options:
|
|
84
86
|
-h, --help Show this quick reference.
|
|
@@ -113,11 +115,17 @@ def main(
|
|
|
113
115
|
args_list = list(sys.argv[1:] if argv is None else argv)
|
|
114
116
|
json_mode = "--json" in args_list
|
|
115
117
|
try:
|
|
118
|
+
if args_list and args_list[0] == "skill":
|
|
119
|
+
if len(args_list) == 1 or "-h" in args_list or "--help" in args_list:
|
|
120
|
+
stdout.write(build_skill_help())
|
|
121
|
+
return 0
|
|
122
|
+
return _run_skill_command(args_list[1:], stdout=stdout)
|
|
123
|
+
synchronize_skill(stderr=stderr, version=__version__)
|
|
116
124
|
if not args_list:
|
|
117
125
|
stdout.write(build_root_help())
|
|
118
126
|
return 0
|
|
119
127
|
if "-h" in args_list or "--help" in args_list:
|
|
120
|
-
stdout.write(
|
|
128
|
+
stdout.write(build_root_help())
|
|
121
129
|
return 0
|
|
122
130
|
if "--version" in args_list:
|
|
123
131
|
if args_list != ["--version"]:
|
|
@@ -131,8 +139,6 @@ def main(
|
|
|
131
139
|
f"{PROGRAM_NAME} {__version__}\n{PROJECT_SUMMARY}\nProject: {PROJECT_URL}\nLicense: {PROJECT_LICENSE}\n"
|
|
132
140
|
)
|
|
133
141
|
return 0
|
|
134
|
-
if args_list[0] == "skill":
|
|
135
|
-
return _run_skill_command(args_list[1:], stdout=stdout)
|
|
136
142
|
args = build_parser().parse_args(args_list)
|
|
137
143
|
return _run(args, stdin=stdin, stdout=stdout)
|
|
138
144
|
except MarkdownDocxError as exc:
|
|
@@ -287,30 +293,56 @@ def _write_error(exc: MarkdownDocxError, *, json_mode: bool, stdout: TextIO, std
|
|
|
287
293
|
|
|
288
294
|
def build_skill_help() -> str:
|
|
289
295
|
return f"""Usage:
|
|
290
|
-
{PROGRAM_NAME} skill install [--skills-dir DIR] [--json]
|
|
296
|
+
{PROGRAM_NAME} skill install [--skills-dir DIR] [--force] [--json]
|
|
291
297
|
{PROGRAM_NAME} skill remove [--skills-dir DIR] [--force] [--json]
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
298
|
+
{PROGRAM_NAME} skill status [--skills-dir DIR] [--json]
|
|
299
|
+
|
|
300
|
+
Install, inspect, or remove the managed `{PROGRAM_NAME}` agent skill.
|
|
301
|
+
The default root is ~/.agents/skills. Status is read-only.
|
|
302
|
+
|
|
303
|
+
Normal commands synchronize an already-installed, pristine older managed skill
|
|
304
|
+
to the running CLI version. They never install a missing skill or downgrade it.
|
|
305
|
+
Modified or unverifiable skills are preserved. To replace managed edits, use:
|
|
306
|
+
uvx {PROGRAM_NAME} skill install --force
|
|
307
|
+
Install --force still refuses unmanaged content. Removal --force may remove
|
|
308
|
+
unmanaged content and extra files in the skill directory.
|
|
309
|
+
|
|
310
|
+
Automatic synchronization uses only the standard directory. Custom directories
|
|
311
|
+
require explicit installs. Local source and editable builds do not synchronize
|
|
312
|
+
automatically. Skill commands never trigger automatic synchronization.
|
|
313
|
+
This does not query a package index, refresh uv, or update the CLI. Updates apply
|
|
314
|
+
to future agent skill loading. A running agent may retain its loaded instructions.
|
|
295
315
|
"""
|
|
296
316
|
|
|
297
317
|
|
|
298
318
|
def _run_skill_command(args_list: list[str], *, stdout: TextIO) -> int:
|
|
299
319
|
parser = CliArgumentParser(prog=f"{PROGRAM_NAME} skill", add_help=False)
|
|
300
|
-
parser.add_argument("action", choices=("install", "remove"))
|
|
320
|
+
parser.add_argument("action", choices=("install", "remove", "status"))
|
|
301
321
|
parser.add_argument("--skills-dir", type=Path)
|
|
302
322
|
parser.add_argument("--force", action="store_true")
|
|
303
323
|
parser.add_argument("--json", action="store_true")
|
|
304
324
|
args = parser.parse_args(args_list)
|
|
305
|
-
if args.action == "
|
|
306
|
-
raise UsageError("--force is valid only with 'skill remove'.")
|
|
325
|
+
if args.action == "status" and args.force:
|
|
326
|
+
raise UsageError("--force is valid only with 'skill install' or 'skill remove'.")
|
|
307
327
|
root = args.skills_dir.resolve() if args.skills_dir else None
|
|
308
|
-
|
|
328
|
+
if args.action == "install":
|
|
329
|
+
result = install_skill(root, force=args.force, version=__version__)
|
|
330
|
+
elif args.action == "remove":
|
|
331
|
+
result = remove_skill(root, force=args.force)
|
|
332
|
+
else:
|
|
333
|
+
result = skill_status(root, version=__version__)
|
|
309
334
|
if args.json:
|
|
310
335
|
stdout.write(json.dumps({"ok": True, "mode": f"skill_{args.action}", **result}, indent=2) + "\n")
|
|
311
336
|
elif args.action == "install":
|
|
312
337
|
verb = "Installed" if result["created"] else "Updated" if result["updated"] else "Already installed"
|
|
313
338
|
stdout.write(f"{verb} {result['path']}\n")
|
|
339
|
+
elif args.action == "status":
|
|
340
|
+
for key, value in result.items():
|
|
341
|
+
label = key.replace("_", " ").capitalize()
|
|
342
|
+
display = (
|
|
343
|
+
"not applicable" if value is None else str(value).lower() if isinstance(value, bool) else str(value)
|
|
344
|
+
)
|
|
345
|
+
stdout.write(f"{label}: {display}\n")
|
|
314
346
|
elif result["removed"]:
|
|
315
347
|
stdout.write(f"Removed {result['path']}\n")
|
|
316
348
|
else:
|
|
@@ -55,7 +55,7 @@ def parse_inline(token: Token, *, line: int, input_path: str | None) -> list[Inl
|
|
|
55
55
|
)
|
|
56
56
|
elif child_type in {"link_open", "link_close"}:
|
|
57
57
|
raise UnsupportedFeatureError(
|
|
58
|
-
"Links require a public python-docx hyperlink creation API and are not supported in 0.
|
|
58
|
+
"Links require a public python-docx hyperlink creation API and are not supported in 0.2.0.",
|
|
59
59
|
line=line,
|
|
60
60
|
input_path=input_path,
|
|
61
61
|
)
|
|
@@ -264,7 +264,7 @@ def _consume_blockquote(tokens: list[Token], index: int, input_path: str) -> tup
|
|
|
264
264
|
index += 1
|
|
265
265
|
while index < len(tokens) and tokens[index].type != "blockquote_close":
|
|
266
266
|
if tokens[index].type != "paragraph_open":
|
|
267
|
-
_unsupported("Blockquotes may contain paragraphs only in 0.
|
|
267
|
+
_unsupported("Blockquotes may contain paragraphs only in 0.2.0.", tokens[index], input_path)
|
|
268
268
|
paragraph, index = _consume_paragraph(tokens, index, input_path)
|
|
269
269
|
if any(fragment.kind == "image" for fragment in paragraph.fragments):
|
|
270
270
|
_unsupported("Images nested in blockquotes are not supported.", opening, input_path)
|
|
@@ -299,7 +299,7 @@ def _consume_list(
|
|
|
299
299
|
if start is not None and int(start) != 1:
|
|
300
300
|
raise ParseError(
|
|
301
301
|
"ordered_list_start_unsupported",
|
|
302
|
-
"Ordered lists must begin with 1 in 0.
|
|
302
|
+
"Ordered lists must begin with 1 in 0.2.0.",
|
|
303
303
|
line=_token_line(opening),
|
|
304
304
|
input_path=input_path,
|
|
305
305
|
)
|