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.
- help2skill-0.1.0/.github/dependabot.yml +10 -0
- help2skill-0.1.0/.github/workflows/ci.yml +49 -0
- help2skill-0.1.0/.github/workflows/publish.yml +53 -0
- help2skill-0.1.0/.gitignore +10 -0
- help2skill-0.1.0/CHANGELOG.md +47 -0
- help2skill-0.1.0/CONTRIBUTING.md +30 -0
- help2skill-0.1.0/LICENSE +21 -0
- help2skill-0.1.0/PKG-INFO +206 -0
- help2skill-0.1.0/README.md +184 -0
- help2skill-0.1.0/SECURITY.md +29 -0
- help2skill-0.1.0/SPEC.md +40 -0
- help2skill-0.1.0/pyproject.toml +59 -0
- help2skill-0.1.0/src/help2skill/__init__.py +243 -0
- help2skill-0.1.0/src/help2skill/__main__.py +3 -0
- help2skill-0.1.0/src/help2skill/_argparse.py +173 -0
- help2skill-0.1.0/src/help2skill/_click.py +256 -0
- help2skill-0.1.0/src/help2skill/cli.py +264 -0
- help2skill-0.1.0/src/help2skill/click.py +98 -0
- help2skill-0.1.0/src/help2skill/model.py +89 -0
- help2skill-0.1.0/src/help2skill/privacy.py +33 -0
- help2skill-0.1.0/src/help2skill/py.typed +0 -0
- help2skill-0.1.0/src/help2skill/render.py +286 -0
- help2skill-0.1.0/src/help2skill/sync.py +448 -0
- help2skill-0.1.0/src/help2skill/testing.py +37 -0
- help2skill-0.1.0/src/help2skill/typer.py +42 -0
- help2skill-0.1.0/tests/conftest.py +4 -0
- help2skill-0.1.0/tests/sample_clis.py +111 -0
- help2skill-0.1.0/tests/test_cli.py +38 -0
- help2skill-0.1.0/tests/test_introspect.py +73 -0
- help2skill-0.1.0/tests/test_render.py +51 -0
- help2skill-0.1.0/tests/test_runtime.py +54 -0
- help2skill-0.1.0/tests/test_sync.py +100 -0
- help2skill-0.1.0/tests/test_user_feedback.py +346 -0
- help2skill-0.1.0/tests/test_v011.py +189 -0
- help2skill-0.1.0/uv.lock +851 -0
|
@@ -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,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).
|
help2skill-0.1.0/LICENSE
ADDED
|
@@ -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.
|
help2skill-0.1.0/SPEC.md
ADDED
|
@@ -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.
|