help2skill 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.
Files changed (35) hide show
  1. help2skill-0.1.0/.github/dependabot.yml +10 -0
  2. help2skill-0.1.0/.github/workflows/ci.yml +49 -0
  3. help2skill-0.1.0/.github/workflows/publish.yml +53 -0
  4. help2skill-0.1.0/.gitignore +10 -0
  5. help2skill-0.1.0/CHANGELOG.md +47 -0
  6. help2skill-0.1.0/CONTRIBUTING.md +30 -0
  7. help2skill-0.1.0/LICENSE +21 -0
  8. help2skill-0.1.0/PKG-INFO +206 -0
  9. help2skill-0.1.0/README.md +184 -0
  10. help2skill-0.1.0/SECURITY.md +29 -0
  11. help2skill-0.1.0/SPEC.md +40 -0
  12. help2skill-0.1.0/pyproject.toml +59 -0
  13. help2skill-0.1.0/src/help2skill/__init__.py +243 -0
  14. help2skill-0.1.0/src/help2skill/__main__.py +3 -0
  15. help2skill-0.1.0/src/help2skill/_argparse.py +173 -0
  16. help2skill-0.1.0/src/help2skill/_click.py +256 -0
  17. help2skill-0.1.0/src/help2skill/cli.py +264 -0
  18. help2skill-0.1.0/src/help2skill/click.py +98 -0
  19. help2skill-0.1.0/src/help2skill/model.py +89 -0
  20. help2skill-0.1.0/src/help2skill/privacy.py +33 -0
  21. help2skill-0.1.0/src/help2skill/py.typed +0 -0
  22. help2skill-0.1.0/src/help2skill/render.py +286 -0
  23. help2skill-0.1.0/src/help2skill/sync.py +448 -0
  24. help2skill-0.1.0/src/help2skill/testing.py +37 -0
  25. help2skill-0.1.0/src/help2skill/typer.py +42 -0
  26. help2skill-0.1.0/tests/conftest.py +4 -0
  27. help2skill-0.1.0/tests/sample_clis.py +111 -0
  28. help2skill-0.1.0/tests/test_cli.py +38 -0
  29. help2skill-0.1.0/tests/test_introspect.py +73 -0
  30. help2skill-0.1.0/tests/test_render.py +51 -0
  31. help2skill-0.1.0/tests/test_runtime.py +54 -0
  32. help2skill-0.1.0/tests/test_sync.py +100 -0
  33. help2skill-0.1.0/tests/test_user_feedback.py +346 -0
  34. help2skill-0.1.0/tests/test_v011.py +189 -0
  35. help2skill-0.1.0/uv.lock +851 -0
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: monthly
7
+ - package-ecosystem: uv
8
+ directory: /
9
+ schedule:
10
+ interval: monthly
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ lint:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+ - uses: astral-sh/setup-uv@v7
17
+ - run: uv sync
18
+ - run: uv run ruff check .
19
+ - run: uv run ruff format --check .
20
+ - run: uv run mypy
21
+
22
+ test:
23
+ runs-on: ${{ matrix.os }}
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ os: [ubuntu-latest]
28
+ python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
29
+ deps: ["latest"]
30
+ include:
31
+ # Oldest supported Click/Typer (before Typer vendored Click).
32
+ - { os: ubuntu-latest, python: "3.10", deps: "click==8.1.7 typer==0.12.5" }
33
+ # Typer just after vendoring Click.
34
+ - { os: ubuntu-latest, python: "3.12", deps: "click==8.2.1 typer==0.26.0" }
35
+ - { os: windows-latest, python: "3.13", deps: "latest" }
36
+ - { os: macos-latest, python: "3.13", deps: "latest" }
37
+ steps:
38
+ - uses: actions/checkout@v7
39
+ - uses: astral-sh/setup-uv@v7
40
+ with:
41
+ python-version: ${{ matrix.python }}
42
+ - name: Test
43
+ shell: bash
44
+ run: |
45
+ if [ "${{ matrix.deps }}" = "latest" ]; then
46
+ uv run --group dev pytest --cov=help2skill --cov-report=term-missing
47
+ else
48
+ uv run --no-dev --with pytest $(printf -- '--with %s ' ${{ matrix.deps }}) pytest
49
+ fi
@@ -0,0 +1,53 @@
1
+ name: Publish
2
+
3
+ # Runs when a GitHub release is published. Uses PyPI trusted publishing:
4
+ # configure the "pypi" environment and the trusted publisher on pypi.org first.
5
+ #
6
+ # PyPI never accepts the same version twice, so nothing is uploaded unless the
7
+ # checks pass and the release tag (e.g. v0.1.0) matches the package version.
8
+ on:
9
+ release:
10
+ types: [published]
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v7
20
+ - uses: astral-sh/setup-uv@v7
21
+ - name: Lint and test
22
+ run: |
23
+ uv run --locked ruff check .
24
+ uv run --locked ruff format --check .
25
+ uv run --locked mypy
26
+ uv run --locked pytest -q
27
+ - run: uv build
28
+ - name: Release tag must match the package version
29
+ env:
30
+ TAG: ${{ github.event.release.tag_name }}
31
+ run: |
32
+ version="${TAG#v}"
33
+ if [ ! -f "dist/help2skill-${version}-py3-none-any.whl" ]; then
34
+ echo "::error::Release tag '$TAG' does not match the built package: $(ls dist)"
35
+ exit 1
36
+ fi
37
+ - uses: actions/upload-artifact@v7
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+
42
+ publish:
43
+ needs: build
44
+ runs-on: ubuntu-latest
45
+ environment: pypi
46
+ permissions:
47
+ id-token: write
48
+ steps:
49
+ - uses: actions/download-artifact@v8
50
+ with:
51
+ name: dist
52
+ path: dist/
53
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .coverage
9
+ .mypy_cache/
10
+ .ruff_cache/
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable changes are listed here. The generated Markdown format is part of the
4
+ public interface: any change to it is called out under **Output**.
5
+
6
+ ## Unreleased
7
+
8
+ ## 0.1.0 (2026-11-07)
9
+ - Introspect Click, Typer (including Typer ≥ 0.26 with vendored Click) and argparse
10
+ into one model.
11
+ - `render_markdown`, `render_skill` (Agent Skills frontmatter validation).
12
+ - `--llms` flag: Click decorator, `add_llms_option`, Typer `LlmsOption` and
13
+ `help2skill.typer.command`.
14
+ - Drift check: stale generated block, unknown subcommands/options and options at the
15
+ wrong level in hand-written examples; default-command groups.
16
+ - `help2skill` command (`markdown`, `skill`, `check`) and
17
+ `help2skill.testing.assert_skill_in_sync`.
18
+ - Large CLIs: `--llms` falls back to an index above 20,000 characters
19
+ (`HELP2SKILL_LLMS_MAX_CHARS`); `SKILL.md` splits the full reference into
20
+ `references/commands.md` above 30,000 characters (`--max-chars`).
21
+ - Drift check understands `uv run`, `uvx`, `uv tool run`, `pipx run`, `poetry run`,
22
+ `pdm run`, `hatch run`, `rye run`, `python -m` and `VAR=value` prefixes.
23
+ - Home directory written as `~` in generated files; warning for other
24
+ machine-specific paths.
25
+ - Lazy commands: `materialize()` placeholders resolved by default; `resolve=` /
26
+ `--resolve` for custom patterns.
27
+ - Changes from testing as a new user, before the first release:
28
+ - Typer integration works without the `click` package (Typer ≥ 0.26).
29
+ - **Output**: counting flags (`-vv`), choices of arguments and tuple values, numeric
30
+ ranges, case-insensitive choices, prompts, optional values, argparse aliases,
31
+ `nargs`, mutually exclusive groups and epilogs are shown; the default command is
32
+ marked; "deprecated" appears once; commands are sorted and type names normalised
33
+ so the output is the same on every Click/Typer version; options repeated in three
34
+ or more commands are listed once; `references/commands.md` no longer repeats the
35
+ index.
36
+ - Program name taken from `--program`, the stored setting, the console script or
37
+ the CLI's own name before the skill name; `markdown` warns when it is unknown.
38
+ - Settings (program, `--max-chars`, `--hide-default`) stored in the begin marker
39
+ and reused by `check`, refreshes and `assert_skill_in_sync` (which also accepts
40
+ `max_chars`, `resolve` and `hide_defaults`).
41
+ - `--hide-default NAME` for defaults computed at run time; the stale-block message
42
+ says when only defaults differ.
43
+ - Drift check: continued lines, short option clusters, values given to flags, invalid
44
+ choices, options moved to a subcommand, hidden commands, "did you mean".
45
+ - Command line: one-line errors (exit 2) instead of tracebacks, `--app-dir`,
46
+ `module:func()` targets, functions not called unless named like a parser, `skill`
47
+ refresh without `--name`, `check` prints a success line and the fix command.
@@ -0,0 +1,30 @@
1
+ # Contributing
2
+
3
+ Thanks for helping! Bug reports, new framework adapters and real-world drift cases
4
+ are especially welcome.
5
+
6
+ ## Setup
7
+ ```console
8
+ uv sync
9
+ uv run pytest
10
+ uv run ruff check . && uv run ruff format --check .
11
+ uv run mypy
12
+ ```
13
+
14
+ ## Pull requests
15
+ - One topic per PR, with a test that fails without the change.
16
+ - The generated Markdown format is a public contract (users commit it and the drift
17
+ check compares it verbatim). If your change alters output, say so in the PR and add a
18
+ `CHANGELOG.md` entry.
19
+ - Keep runtime dependencies at zero.
20
+
21
+ ## Sign-off (DCO)
22
+ By contributing you agree that your contribution is licensed under the project's
23
+ [MIT license](LICENSE) and that you have the right to submit it, as described in the
24
+ [Developer Certificate of Origin](https://developercertificate.org/).
25
+ Sign each commit with `git commit -s`, which adds a `Signed-off-by:` line.
26
+
27
+ ## AI-assisted contributions
28
+ AI tools are welcome. Mention them in the PR description, and make sure you have read
29
+ and understood every line you submit. This project itself was built with AI assistance
30
+ (Claude).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 huysonmind
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,206 @@
1
+ Metadata-Version: 2.5
2
+ Name: help2skill
3
+ Version: 0.1.0
4
+ Summary: Make Python CLIs ready for LLM agents: --llms output, SKILL.md generation and drift checks for Click, Typer and argparse.
5
+ Project-URL: Homepage, https://github.com/huysonmind/help2skill
6
+ Project-URL: Issues, https://github.com/huysonmind/help2skill/issues
7
+ Project-URL: Changelog, https://github.com/huysonmind/help2skill/blob/main/CHANGELOG.md
8
+ Author-email: huysonmind <350970572+huysonmind@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent-skills,agents,argparse,cli,click,llm,llms-txt,skill-md,typer
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Software Development :: Documentation
18
+ Classifier: Topic :: Software Development :: User Interfaces
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+
23
+ # help2skill
24
+
25
+ > **Status: alpha.** The output format may still change before 1.0.
26
+
27
+ `--help` for LLM agents. Make a Click, Typer or argparse CLI easy for coding agents
28
+ (Claude Code, Codex, Cursor, Gemini CLI…) to use correctly:
29
+
30
+ - **`--llms` flag**: prints a compact Markdown reference of the command tree (every
31
+ subcommand, argument, option, choice, default and env var), generated from the CLI
32
+ itself, so it never goes stale.
33
+ - **`SKILL.md` export**: writes an [Agent Skills](https://agentskills.io) file whose
34
+ reference block is regenerated from the CLI while your hand-written guidance is kept.
35
+ - **Drift check for CI**: fails when the generated block is stale, or when hand-written
36
+ examples mention a subcommand or option that does not exist (or an option placed at
37
+ the wrong level, e.g. a group option after the subcommand).
38
+
39
+ No runtime dependencies. Works with Typer ≥ 0.26 (vendored Click), where code that does
40
+ `isinstance(cmd, click.Group)` silently finds nothing.
41
+
42
+ ## Quick start
43
+
44
+ ### Click
45
+
46
+ ```python
47
+ import click
48
+ from help2skill.click import llms_option
49
+
50
+
51
+ @click.group()
52
+ @llms_option()
53
+ def cli(): ...
54
+ ```
55
+
56
+ ```console
57
+ $ mytool --llms
58
+ ```
59
+
60
+ `llms_option()` adds the flag to the root only, which prints the whole tree. To get
61
+ `--llms` on every subcommand too (`mytool db --llms` prints just that subtree):
62
+
63
+ ```python
64
+ from help2skill.click import add_llms_option
65
+
66
+ add_llms_option(cli)
67
+ ```
68
+
69
+ ### Typer
70
+
71
+ Root flag, no change to your entry point:
72
+
73
+ ```python
74
+ import typer
75
+ from help2skill.typer import LlmsOption
76
+
77
+ app = typer.Typer()
78
+
79
+
80
+ @app.callback()
81
+ def main(llms: LlmsOption = False): ...
82
+ ```
83
+
84
+ Or `--llms` at every level: build the command once and make it your console script.
85
+
86
+ ```python
87
+ # mytool/cli.py
88
+ import help2skill.typer
89
+
90
+ main = help2skill.typer.command(app, name="mytool")
91
+ ```
92
+
93
+ ```toml
94
+ # pyproject.toml
95
+ [project.scripts]
96
+ mytool = "mytool.cli:main"
97
+ ```
98
+
99
+ No `click` package is needed with Typer ≥ 0.26, which ships its own copy.
100
+
101
+ ### argparse
102
+
103
+ argparse has no hook for an extra flag, so use the command line (or the Python API):
104
+
105
+ ```console
106
+ $ help2skill markdown mytool.cli:parser
107
+ $ help2skill markdown "mytool.cli:build_parser()" # a function that returns the parser
108
+ ```
109
+
110
+ ### Large CLIs
111
+
112
+ `--llms` prints the full reference when it fits in 20,000 characters (about 5k tokens).
113
+ Above that it prints the top-level help plus an index of every command, and tells the
114
+ agent how to ask for one command (`mytool <command> --llms`, or `--help` when only the
115
+ root has the flag). Set `HELP2SKILL_LLMS_MAX_CHARS=0` for everything. Options repeated
116
+ identically in three or more commands are described once, under "Shared options".
117
+
118
+ ### Lazy commands
119
+
120
+ Click's documented `LazyGroup` pattern works as is. Placeholder commands with a
121
+ `materialize()` method are loaded automatically; for anything else pass
122
+ `resolve=` (Python API) or `--resolve module:func` (command line).
123
+
124
+ ## SKILL.md and drift checks
125
+
126
+ ```console
127
+ $ help2skill skill mytool.cli:app --name mytool --description "Use mytool to ..." \
128
+ -o src/mytool/.agents/skills/mytool/SKILL.md
129
+ $ help2skill check mytool.cli:app src/mytool/.agents/skills/mytool/SKILL.md
130
+ ```
131
+
132
+ `skill` creates the file, or refreshes only the block between
133
+ `<!-- help2skill:begin … -->` and `<!-- help2skill:end -->` if it already exists
134
+ (`--name` is then optional). Write your own guidance above or below the block.
135
+
136
+ **Program name.** Commands are written with the name users type. help2skill uses
137
+ `--program` if given, else the installed console script that points at the target,
138
+ else the CLI's own name (`@click.group(name="mytool")`, `ArgumentParser(prog="mytool")`),
139
+ else the skill name. `markdown` warns when it can only guess.
140
+
141
+ **Settings are stored in the file.** The begin marker records the program name,
142
+ `--max-chars` and `--hide-default`, so `check`, a refresh and the pytest helper reuse
143
+ them without repeating any option.
144
+
145
+ **Stable in CI.** The output does not depend on the machine or on the Click/Typer
146
+ version: commands are sorted, type names are normalised and your home directory is
147
+ written as `~` (other machine-specific paths trigger a warning). A default computed at
148
+ run time (CPU count, today's date) would still differ, so leave it out:
149
+
150
+ ```console
151
+ $ help2skill skill mytool.cli:app -o SKILL.md --hide-default workers --hide-default --since
152
+ ```
153
+
154
+ When the reference is longer than 30,000 characters, `SKILL.md` keeps an index and the
155
+ full reference goes to `references/commands.md` beside it (fully generated; agents read
156
+ it on demand). Change the limit with `--max-chars` (0 = never split).
157
+
158
+ **What the check catches** in code spans and fenced blocks of your hand-written text:
159
+ unknown subcommands and options (with "did you mean"), options at the wrong level,
160
+ values not among an option's or argument's choices, values given to flags, and
161
+ single-letter clusters such as `-fq`. Lines continued with `\` are joined, and lines
162
+ are recognised with launchers in front: `uv run [--with …] mytool …`, `uvx`,
163
+ `pipx run`, `poetry run`, `python -m mytool`, `VAR=value mytool …`.
164
+
165
+ Shipping the skill inside your package under `.agents/skills/<name>/` makes it
166
+ installable with [library-skills](https://github.com/tiangolo/library-skills). With
167
+ setuptools, add the folder to `package-data`; hatchling includes it by default.
168
+
169
+ In pytest (build the path from `__file__` so the test runs from any directory):
170
+
171
+ ```python
172
+ from pathlib import Path
173
+
174
+ from help2skill.testing import assert_skill_in_sync
175
+ from mytool.cli import app
176
+
177
+ SKILL = Path(__file__).parents[1] / "src/mytool/.agents/skills/mytool/SKILL.md"
178
+
179
+
180
+ def test_skill_in_sync():
181
+ assert_skill_in_sync(app, SKILL)
182
+ ```
183
+
184
+ With a `src/` layout that is not installed, pass `--app-dir src` to the command line.
185
+
186
+ ## Python API
187
+
188
+ ```python
189
+ import help2skill
190
+
191
+ tree = help2skill.introspect(app, "mytool") # Click, Typer or argparse
192
+ print(help2skill.render_markdown(tree))
193
+ help2skill.write_skill(app, "SKILL.md", name="mytool")
194
+ problems = help2skill.check_skill_file(app, "SKILL.md")
195
+ ```
196
+
197
+ ## Security
198
+
199
+ `help2skill` imports the module you point it at, the same as running your CLI. It calls
200
+ a function only when you write `module:func()` or the function's name contains
201
+ "parser"; it never calls your `main()`. Generated files copy your help text verbatim, so review them
202
+ for machine-specific values before committing. See [SECURITY.md](https://github.com/huysonmind/help2skill/blob/main/SECURITY.md).
203
+
204
+ ## License
205
+
206
+ [MIT](https://github.com/huysonmind/help2skill/blob/main/LICENSE). Built with AI assistance (Claude).
@@ -0,0 +1,184 @@
1
+ # help2skill
2
+
3
+ > **Status: alpha.** The output format may still change before 1.0.
4
+
5
+ `--help` for LLM agents. Make a Click, Typer or argparse CLI easy for coding agents
6
+ (Claude Code, Codex, Cursor, Gemini CLI…) to use correctly:
7
+
8
+ - **`--llms` flag**: prints a compact Markdown reference of the command tree (every
9
+ subcommand, argument, option, choice, default and env var), generated from the CLI
10
+ itself, so it never goes stale.
11
+ - **`SKILL.md` export**: writes an [Agent Skills](https://agentskills.io) file whose
12
+ reference block is regenerated from the CLI while your hand-written guidance is kept.
13
+ - **Drift check for CI**: fails when the generated block is stale, or when hand-written
14
+ examples mention a subcommand or option that does not exist (or an option placed at
15
+ the wrong level, e.g. a group option after the subcommand).
16
+
17
+ No runtime dependencies. Works with Typer ≥ 0.26 (vendored Click), where code that does
18
+ `isinstance(cmd, click.Group)` silently finds nothing.
19
+
20
+ ## Quick start
21
+
22
+ ### Click
23
+
24
+ ```python
25
+ import click
26
+ from help2skill.click import llms_option
27
+
28
+
29
+ @click.group()
30
+ @llms_option()
31
+ def cli(): ...
32
+ ```
33
+
34
+ ```console
35
+ $ mytool --llms
36
+ ```
37
+
38
+ `llms_option()` adds the flag to the root only, which prints the whole tree. To get
39
+ `--llms` on every subcommand too (`mytool db --llms` prints just that subtree):
40
+
41
+ ```python
42
+ from help2skill.click import add_llms_option
43
+
44
+ add_llms_option(cli)
45
+ ```
46
+
47
+ ### Typer
48
+
49
+ Root flag, no change to your entry point:
50
+
51
+ ```python
52
+ import typer
53
+ from help2skill.typer import LlmsOption
54
+
55
+ app = typer.Typer()
56
+
57
+
58
+ @app.callback()
59
+ def main(llms: LlmsOption = False): ...
60
+ ```
61
+
62
+ Or `--llms` at every level: build the command once and make it your console script.
63
+
64
+ ```python
65
+ # mytool/cli.py
66
+ import help2skill.typer
67
+
68
+ main = help2skill.typer.command(app, name="mytool")
69
+ ```
70
+
71
+ ```toml
72
+ # pyproject.toml
73
+ [project.scripts]
74
+ mytool = "mytool.cli:main"
75
+ ```
76
+
77
+ No `click` package is needed with Typer ≥ 0.26, which ships its own copy.
78
+
79
+ ### argparse
80
+
81
+ argparse has no hook for an extra flag, so use the command line (or the Python API):
82
+
83
+ ```console
84
+ $ help2skill markdown mytool.cli:parser
85
+ $ help2skill markdown "mytool.cli:build_parser()" # a function that returns the parser
86
+ ```
87
+
88
+ ### Large CLIs
89
+
90
+ `--llms` prints the full reference when it fits in 20,000 characters (about 5k tokens).
91
+ Above that it prints the top-level help plus an index of every command, and tells the
92
+ agent how to ask for one command (`mytool <command> --llms`, or `--help` when only the
93
+ root has the flag). Set `HELP2SKILL_LLMS_MAX_CHARS=0` for everything. Options repeated
94
+ identically in three or more commands are described once, under "Shared options".
95
+
96
+ ### Lazy commands
97
+
98
+ Click's documented `LazyGroup` pattern works as is. Placeholder commands with a
99
+ `materialize()` method are loaded automatically; for anything else pass
100
+ `resolve=` (Python API) or `--resolve module:func` (command line).
101
+
102
+ ## SKILL.md and drift checks
103
+
104
+ ```console
105
+ $ help2skill skill mytool.cli:app --name mytool --description "Use mytool to ..." \
106
+ -o src/mytool/.agents/skills/mytool/SKILL.md
107
+ $ help2skill check mytool.cli:app src/mytool/.agents/skills/mytool/SKILL.md
108
+ ```
109
+
110
+ `skill` creates the file, or refreshes only the block between
111
+ `<!-- help2skill:begin … -->` and `<!-- help2skill:end -->` if it already exists
112
+ (`--name` is then optional). Write your own guidance above or below the block.
113
+
114
+ **Program name.** Commands are written with the name users type. help2skill uses
115
+ `--program` if given, else the installed console script that points at the target,
116
+ else the CLI's own name (`@click.group(name="mytool")`, `ArgumentParser(prog="mytool")`),
117
+ else the skill name. `markdown` warns when it can only guess.
118
+
119
+ **Settings are stored in the file.** The begin marker records the program name,
120
+ `--max-chars` and `--hide-default`, so `check`, a refresh and the pytest helper reuse
121
+ them without repeating any option.
122
+
123
+ **Stable in CI.** The output does not depend on the machine or on the Click/Typer
124
+ version: commands are sorted, type names are normalised and your home directory is
125
+ written as `~` (other machine-specific paths trigger a warning). A default computed at
126
+ run time (CPU count, today's date) would still differ, so leave it out:
127
+
128
+ ```console
129
+ $ help2skill skill mytool.cli:app -o SKILL.md --hide-default workers --hide-default --since
130
+ ```
131
+
132
+ When the reference is longer than 30,000 characters, `SKILL.md` keeps an index and the
133
+ full reference goes to `references/commands.md` beside it (fully generated; agents read
134
+ it on demand). Change the limit with `--max-chars` (0 = never split).
135
+
136
+ **What the check catches** in code spans and fenced blocks of your hand-written text:
137
+ unknown subcommands and options (with "did you mean"), options at the wrong level,
138
+ values not among an option's or argument's choices, values given to flags, and
139
+ single-letter clusters such as `-fq`. Lines continued with `\` are joined, and lines
140
+ are recognised with launchers in front: `uv run [--with …] mytool …`, `uvx`,
141
+ `pipx run`, `poetry run`, `python -m mytool`, `VAR=value mytool …`.
142
+
143
+ Shipping the skill inside your package under `.agents/skills/<name>/` makes it
144
+ installable with [library-skills](https://github.com/tiangolo/library-skills). With
145
+ setuptools, add the folder to `package-data`; hatchling includes it by default.
146
+
147
+ In pytest (build the path from `__file__` so the test runs from any directory):
148
+
149
+ ```python
150
+ from pathlib import Path
151
+
152
+ from help2skill.testing import assert_skill_in_sync
153
+ from mytool.cli import app
154
+
155
+ SKILL = Path(__file__).parents[1] / "src/mytool/.agents/skills/mytool/SKILL.md"
156
+
157
+
158
+ def test_skill_in_sync():
159
+ assert_skill_in_sync(app, SKILL)
160
+ ```
161
+
162
+ With a `src/` layout that is not installed, pass `--app-dir src` to the command line.
163
+
164
+ ## Python API
165
+
166
+ ```python
167
+ import help2skill
168
+
169
+ tree = help2skill.introspect(app, "mytool") # Click, Typer or argparse
170
+ print(help2skill.render_markdown(tree))
171
+ help2skill.write_skill(app, "SKILL.md", name="mytool")
172
+ problems = help2skill.check_skill_file(app, "SKILL.md")
173
+ ```
174
+
175
+ ## Security
176
+
177
+ `help2skill` imports the module you point it at, the same as running your CLI. It calls
178
+ a function only when you write `module:func()` or the function's name contains
179
+ "parser"; it never calls your `main()`. Generated files copy your help text verbatim, so review them
180
+ for machine-specific values before committing. See [SECURITY.md](https://github.com/huysonmind/help2skill/blob/main/SECURITY.md).
181
+
182
+ ## License
183
+
184
+ [MIT](https://github.com/huysonmind/help2skill/blob/main/LICENSE). Built with AI assistance (Claude).
@@ -0,0 +1,29 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+ help2skill is in alpha. Only the latest release receives fixes.
5
+
6
+ ## Reporting a vulnerability
7
+ Please report privately through GitHub:
8
+ **Security → Report a vulnerability** on https://github.com/huysonmind/help2skill
9
+ (private vulnerability reporting). Do not open a public issue.
10
+
11
+ You should get a first answer within 7 days.
12
+
13
+ ## What help2skill executes
14
+ - `help2skill markdown|skill|check <module:attr>` **imports your module** to read the
15
+ command tree, exactly like running your CLI would. Only point it at code you trust.
16
+ - If `attr` is a plain function (for example `build_parser`), it is **called** to obtain
17
+ the parser.
18
+ - The `--llms` flag only reads the command tree that is already loaded; it does not run
19
+ any command callback.
20
+
21
+ ## Data in generated files
22
+ Generated Markdown copies your CLI's help text verbatim. If help text is computed at
23
+ runtime (for example a default path under your home directory), that value ends up in
24
+ the output. Review `SKILL.md` before committing it.
25
+
26
+ ## Releases
27
+ Releases are published to PyPI from GitHub Actions with
28
+ [trusted publishing](https://docs.pypi.org/trusted-publishers/); no long-lived API
29
+ token is stored anywhere.
@@ -0,0 +1,40 @@
1
+ # help2skill: short spec (v0.1)
2
+
3
+ ## Problem
4
+ Coding agents call CLIs constantly but learn them from stale training data or by
5
+ guessing. In 2026 large Python CLIs started shipping agent docs by hand:
6
+ Hugging Face (`hf skills`, generated from the Click tree), Modal (`modal skills`),
7
+ SkyPilot (`agent/scripts/generate_references.py`), AWS CLI (`aws configure agent-toolkit`),
8
+ dbt-charts, Keboola. Each wrote its own generator; smaller projects copy-paste
9
+ `gen_skill.py` scripts. Typer ≥ 0.26 vendors Click, which silently breaks those
10
+ scripts' `isinstance(cmd, click.Group)` checks.
11
+
12
+ ## Scope of v0.1
13
+ 1. **Introspection** of Click, Typer (old and vendored-Click) and argparse into one
14
+ neutral model (`Command`, `Param`). Duck-typed; zero runtime dependencies.
15
+ 2. **Markdown reference** (`render_markdown`): full path in every heading, an index
16
+ of all commands at the root, per-param type / required / repeatable / default /
17
+ env var / help. Deterministic output.
18
+ 3. **`--llms` flag**: Click decorator, `add_llms_option` for any built tree,
19
+ Typer `LlmsOption` (root) and `help2skill.typer.command(app)` (every level).
20
+ 4. **SKILL.md**: frontmatter validated against Agent Skills rules (name ≤ 64,
21
+ `[a-z0-9-]`, description ≤ 1024, no angle brackets); generated block between
22
+ markers, hand-written text preserved on refresh.
23
+ 5. **Drift check**: stale block; hand-written examples that use a missing subcommand,
24
+ a missing option, or an option at the wrong level. Handles default-command groups.
25
+ CLI `help2skill check` (exit 1) and `help2skill.testing.assert_skill_in_sync`.
26
+
27
+ 6. **Size budget**: `--llms` index above 20,000 characters; SKILL.md index plus
28
+ `references/commands.md` above 30,000 characters.
29
+ 7. **Portability**: home directory written as `~`; warnings for other machine paths;
30
+ launcher prefixes (`uv run`, `python -m`, …) understood by the drift check;
31
+ pluggable `resolve` for lazy placeholder commands.
32
+
33
+ ## Non-goals (v0.1)
34
+ - Installing skills into agent folders (use library-skills).
35
+ - Detecting the calling agent / auto JSON output (planned, via existing detectors).
36
+ - Static analysis without importing the CLI (claude-skill-forge does that).
37
+
38
+ ## Output contract
39
+ Markdown output is a public interface: changes to it are noted in the changelog,
40
+ because users commit generated blocks and the drift check compares them verbatim.