completion-ai 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.
- completion_ai-0.1.0/.gitignore +11 -0
- completion_ai-0.1.0/LICENSE +21 -0
- completion_ai-0.1.0/PKG-INFO +164 -0
- completion_ai-0.1.0/README.md +135 -0
- completion_ai-0.1.0/completion_ai/cli.py +198 -0
- completion_ai-0.1.0/completion_ai/crawler.py +61 -0
- completion_ai-0.1.0/completion_ai/llm.py +130 -0
- completion_ai-0.1.0/completion_ai/renderer.py +141 -0
- completion_ai-0.1.0/pyproject.toml +51 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yangwb
|
|
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,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: completion-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LLM-powered zsh completion scaffolding for arbitrary CLIs
|
|
5
|
+
Project-URL: Homepage, https://github.com/Yangeyu/completion-ai
|
|
6
|
+
Project-URL: Repository, https://github.com/Yangeyu/completion-ai
|
|
7
|
+
Project-URL: Issues, https://github.com/Yangeyu/completion-ai/issues
|
|
8
|
+
Author-email: yangwb <binyang617@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,completion,dashscope,llm,qwen,zsh
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development
|
|
23
|
+
Classifier: Topic :: System :: Shells
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: httpx[socks]>=0.27.0
|
|
27
|
+
Requires-Dist: openai>=1.40.0
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# completion-ai
|
|
31
|
+
|
|
32
|
+
LLM-powered zsh completion scaffolding for arbitrary CLIs. Point it at any
|
|
33
|
+
command on your `PATH` and it will:
|
|
34
|
+
|
|
35
|
+
1. Recursively run `<cmd> --help` (and discovered subcommand `--help`s).
|
|
36
|
+
Subcommand discovery is done by the LLM, not regex — so `install [options]`,
|
|
37
|
+
`plugin|plugins`, and other oddly-formatted entries are picked up.
|
|
38
|
+
2. Ask Qwen (via DashScope OpenAI-compatible API) to extract a structured
|
|
39
|
+
schema of flags, subcommands, and positional arguments.
|
|
40
|
+
3. Render a `#compdef` zsh completion script using a deterministic template
|
|
41
|
+
(the LLM never writes shell directly).
|
|
42
|
+
4. Run `zsh -n` against the result and warn on syntax errors.
|
|
43
|
+
|
|
44
|
+
The output is meant to be **reviewed by a human** before installation — it's
|
|
45
|
+
a scaffold, not a runtime completion engine.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
Recommended (global CLI, isolated venv managed by uv):
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
uv tool install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Other options:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
uv pip install -e . # into the current uv venv
|
|
59
|
+
pip install -e . # into the active python (conda, system, ...)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires `DASHSCOPE_API_KEY` in the environment.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
completion-ai claude # writes ./_claude
|
|
68
|
+
completion-ai claude --install # install straight into oh-my-zsh
|
|
69
|
+
completion-ai claude -o ~/.zsh/completions/_claude
|
|
70
|
+
completion-ai docker --depth 3 -v # crawl deeper, verbose
|
|
71
|
+
completion-ai gh --dump-schema gh.json # also save intermediate schema
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Options
|
|
75
|
+
|
|
76
|
+
| Flag | Default | Notes |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `-o, --output PATH` | `./_<cmd>` | Where to write the completion script |
|
|
79
|
+
| `-d, --depth N` | `2` | How deep to crawl subcommand help |
|
|
80
|
+
| `--model ID` | `qwen-plus` | Override via `COMPLETION_AI_MODEL` too |
|
|
81
|
+
| `--dump-schema PATH` | — | Also save the JSON the LLM produced |
|
|
82
|
+
| `--no-syntax-check` | off | Skip `zsh -n` validation |
|
|
83
|
+
| `--install` | off | Install into `$ZSH/custom/plugins/completion-ai/` |
|
|
84
|
+
| `-v, --verbose` | off | Progress logs to stderr |
|
|
85
|
+
|
|
86
|
+
### Environment
|
|
87
|
+
|
|
88
|
+
- `DASHSCOPE_API_KEY` — required
|
|
89
|
+
- `COMPLETION_AI_MODEL` — defaults to `qwen-plus`
|
|
90
|
+
- `COMPLETION_AI_BASE_URL` — defaults to DashScope OpenAI-compatible endpoint
|
|
91
|
+
- `ZSH` — oh-my-zsh root, used by `--install` (defaults to `~/.oh-my-zsh`)
|
|
92
|
+
|
|
93
|
+
## Installing the generated completion
|
|
94
|
+
|
|
95
|
+
### Option A — oh-my-zsh users (recommended)
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
completion-ai claude --install
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This creates `$ZSH/custom/plugins/completion-ai/_claude` plus a
|
|
102
|
+
plugin stub. **First time only**, add the plugin to `~/.zshrc`:
|
|
103
|
+
|
|
104
|
+
```zsh
|
|
105
|
+
plugins=(git ... completion-ai)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Then refresh:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
rm -f ~/.zcompdump* && exec zsh
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Subsequent `completion-ai <cmd> --install` calls drop new completions into the
|
|
115
|
+
same plugin directory — no zshrc edits needed.
|
|
116
|
+
|
|
117
|
+
### Option B — plain zsh
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
completion-ai claude -o ~/.zsh/completions/_claude
|
|
121
|
+
|
|
122
|
+
# in ~/.zshrc (one-time):
|
|
123
|
+
fpath=(~/.zsh/completions $fpath)
|
|
124
|
+
autoload -U compinit && compinit
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## How it works
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
[crawler] run `<cmd> --help`
|
|
131
|
+
↓
|
|
132
|
+
[llm] discover subcommand names from help text (1 call per node)
|
|
133
|
+
↓
|
|
134
|
+
[crawler] recursively run discovered subcommands' --help
|
|
135
|
+
↓
|
|
136
|
+
[llm] extract structured JSON schema from all help texts (1 call)
|
|
137
|
+
↓
|
|
138
|
+
[renderer] JSON → #compdef zsh template (deterministic, not LLM)
|
|
139
|
+
↓
|
|
140
|
+
[validator] zsh -n syntax check
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
For `depth=2`, that's **2 LLM calls** total (1 discover + 1 extract).
|
|
144
|
+
|
|
145
|
+
## Limitations
|
|
146
|
+
|
|
147
|
+
- Static only: dynamic completions (e.g. `git checkout <branch>`) are not
|
|
148
|
+
generated — the script may suggest a hint type (`branch`, `host`, ...) but
|
|
149
|
+
won't query live state.
|
|
150
|
+
- The LLM may miss hidden flags or mislabel value hints. Always diff against
|
|
151
|
+
`<cmd> --help` before trusting.
|
|
152
|
+
- Only zsh for now.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
git clone <repo> && cd completion-ai
|
|
158
|
+
uv venv && source .venv/bin/activate
|
|
159
|
+
uv pip install -e .
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# completion-ai
|
|
2
|
+
|
|
3
|
+
LLM-powered zsh completion scaffolding for arbitrary CLIs. Point it at any
|
|
4
|
+
command on your `PATH` and it will:
|
|
5
|
+
|
|
6
|
+
1. Recursively run `<cmd> --help` (and discovered subcommand `--help`s).
|
|
7
|
+
Subcommand discovery is done by the LLM, not regex — so `install [options]`,
|
|
8
|
+
`plugin|plugins`, and other oddly-formatted entries are picked up.
|
|
9
|
+
2. Ask Qwen (via DashScope OpenAI-compatible API) to extract a structured
|
|
10
|
+
schema of flags, subcommands, and positional arguments.
|
|
11
|
+
3. Render a `#compdef` zsh completion script using a deterministic template
|
|
12
|
+
(the LLM never writes shell directly).
|
|
13
|
+
4. Run `zsh -n` against the result and warn on syntax errors.
|
|
14
|
+
|
|
15
|
+
The output is meant to be **reviewed by a human** before installation — it's
|
|
16
|
+
a scaffold, not a runtime completion engine.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
Recommended (global CLI, isolated venv managed by uv):
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
uv tool install -e .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Other options:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
uv pip install -e . # into the current uv venv
|
|
30
|
+
pip install -e . # into the active python (conda, system, ...)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires `DASHSCOPE_API_KEY` in the environment.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
completion-ai claude # writes ./_claude
|
|
39
|
+
completion-ai claude --install # install straight into oh-my-zsh
|
|
40
|
+
completion-ai claude -o ~/.zsh/completions/_claude
|
|
41
|
+
completion-ai docker --depth 3 -v # crawl deeper, verbose
|
|
42
|
+
completion-ai gh --dump-schema gh.json # also save intermediate schema
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Options
|
|
46
|
+
|
|
47
|
+
| Flag | Default | Notes |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `-o, --output PATH` | `./_<cmd>` | Where to write the completion script |
|
|
50
|
+
| `-d, --depth N` | `2` | How deep to crawl subcommand help |
|
|
51
|
+
| `--model ID` | `qwen-plus` | Override via `COMPLETION_AI_MODEL` too |
|
|
52
|
+
| `--dump-schema PATH` | — | Also save the JSON the LLM produced |
|
|
53
|
+
| `--no-syntax-check` | off | Skip `zsh -n` validation |
|
|
54
|
+
| `--install` | off | Install into `$ZSH/custom/plugins/completion-ai/` |
|
|
55
|
+
| `-v, --verbose` | off | Progress logs to stderr |
|
|
56
|
+
|
|
57
|
+
### Environment
|
|
58
|
+
|
|
59
|
+
- `DASHSCOPE_API_KEY` — required
|
|
60
|
+
- `COMPLETION_AI_MODEL` — defaults to `qwen-plus`
|
|
61
|
+
- `COMPLETION_AI_BASE_URL` — defaults to DashScope OpenAI-compatible endpoint
|
|
62
|
+
- `ZSH` — oh-my-zsh root, used by `--install` (defaults to `~/.oh-my-zsh`)
|
|
63
|
+
|
|
64
|
+
## Installing the generated completion
|
|
65
|
+
|
|
66
|
+
### Option A — oh-my-zsh users (recommended)
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
completion-ai claude --install
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This creates `$ZSH/custom/plugins/completion-ai/_claude` plus a
|
|
73
|
+
plugin stub. **First time only**, add the plugin to `~/.zshrc`:
|
|
74
|
+
|
|
75
|
+
```zsh
|
|
76
|
+
plugins=(git ... completion-ai)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Then refresh:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
rm -f ~/.zcompdump* && exec zsh
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Subsequent `completion-ai <cmd> --install` calls drop new completions into the
|
|
86
|
+
same plugin directory — no zshrc edits needed.
|
|
87
|
+
|
|
88
|
+
### Option B — plain zsh
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
completion-ai claude -o ~/.zsh/completions/_claude
|
|
92
|
+
|
|
93
|
+
# in ~/.zshrc (one-time):
|
|
94
|
+
fpath=(~/.zsh/completions $fpath)
|
|
95
|
+
autoload -U compinit && compinit
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## How it works
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
[crawler] run `<cmd> --help`
|
|
102
|
+
↓
|
|
103
|
+
[llm] discover subcommand names from help text (1 call per node)
|
|
104
|
+
↓
|
|
105
|
+
[crawler] recursively run discovered subcommands' --help
|
|
106
|
+
↓
|
|
107
|
+
[llm] extract structured JSON schema from all help texts (1 call)
|
|
108
|
+
↓
|
|
109
|
+
[renderer] JSON → #compdef zsh template (deterministic, not LLM)
|
|
110
|
+
↓
|
|
111
|
+
[validator] zsh -n syntax check
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For `depth=2`, that's **2 LLM calls** total (1 discover + 1 extract).
|
|
115
|
+
|
|
116
|
+
## Limitations
|
|
117
|
+
|
|
118
|
+
- Static only: dynamic completions (e.g. `git checkout <branch>`) are not
|
|
119
|
+
generated — the script may suggest a hint type (`branch`, `host`, ...) but
|
|
120
|
+
won't query live state.
|
|
121
|
+
- The LLM may miss hidden flags or mislabel value hints. Always diff against
|
|
122
|
+
`<cmd> --help` before trusting.
|
|
123
|
+
- Only zsh for now.
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
git clone <repo> && cd completion-ai
|
|
129
|
+
uv venv && source .venv/bin/activate
|
|
130
|
+
uv pip install -e .
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""completion-ai CLI entry point."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
OMZ_PLUGIN_NAME = "completion-ai"
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .crawler import crawl
|
|
16
|
+
from .llm import DEFAULT_MODEL, discover_subcommands, extract_schema
|
|
17
|
+
from .renderer import render_zsh
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
21
|
+
p = argparse.ArgumentParser(
|
|
22
|
+
prog="completion-ai",
|
|
23
|
+
description="Generate zsh completion scaffolding for any CLI using an LLM.",
|
|
24
|
+
)
|
|
25
|
+
p.add_argument("command", help="Target CLI command (must be on PATH)")
|
|
26
|
+
p.add_argument(
|
|
27
|
+
"-o", "--output", type=Path, default=None,
|
|
28
|
+
help="Output path (default: ./_<command>)",
|
|
29
|
+
)
|
|
30
|
+
p.add_argument(
|
|
31
|
+
"-d", "--depth", type=int, default=2,
|
|
32
|
+
help="Max subcommand crawl depth (default: 2)",
|
|
33
|
+
)
|
|
34
|
+
p.add_argument(
|
|
35
|
+
"--model", default=DEFAULT_MODEL,
|
|
36
|
+
help=f"LLM model id (default: {DEFAULT_MODEL}; env COMPLETION_AI_MODEL)",
|
|
37
|
+
)
|
|
38
|
+
p.add_argument(
|
|
39
|
+
"--dump-schema", type=Path, default=None,
|
|
40
|
+
help="Also write the intermediate JSON schema here",
|
|
41
|
+
)
|
|
42
|
+
p.add_argument(
|
|
43
|
+
"--no-syntax-check", action="store_true",
|
|
44
|
+
help="Skip the zsh -n syntax validation",
|
|
45
|
+
)
|
|
46
|
+
p.add_argument(
|
|
47
|
+
"--install", action="store_true",
|
|
48
|
+
help=(
|
|
49
|
+
"Install the generated completion into the oh-my-zsh plugin "
|
|
50
|
+
f"'{OMZ_PLUGIN_NAME}' (overrides --output)."
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
54
|
+
p.add_argument("--version", action="version", version=f"completion-ai {__version__}")
|
|
55
|
+
return p
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _syntax_check(script: str) -> tuple[bool, str]:
|
|
59
|
+
with tempfile.NamedTemporaryFile("w", suffix=".zsh", delete=False) as f:
|
|
60
|
+
f.write(script)
|
|
61
|
+
path = f.name
|
|
62
|
+
try:
|
|
63
|
+
r = subprocess.run(
|
|
64
|
+
["zsh", "-n", path],
|
|
65
|
+
capture_output=True, text=True, timeout=5,
|
|
66
|
+
)
|
|
67
|
+
return r.returncode == 0, (r.stderr or r.stdout).strip()
|
|
68
|
+
except FileNotFoundError:
|
|
69
|
+
return True, "zsh not found, skipping syntax check"
|
|
70
|
+
finally:
|
|
71
|
+
Path(path).unlink(missing_ok=True)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main(argv: list[str] | None = None) -> int:
|
|
75
|
+
args = build_parser().parse_args(argv)
|
|
76
|
+
cmd = args.command
|
|
77
|
+
|
|
78
|
+
def log(msg: str) -> None:
|
|
79
|
+
if args.verbose:
|
|
80
|
+
print(f"[completion-ai] {msg}", file=sys.stderr)
|
|
81
|
+
|
|
82
|
+
discover_calls = [0]
|
|
83
|
+
|
|
84
|
+
def _discover(help_text: str) -> list[str]:
|
|
85
|
+
discover_calls[0] += 1
|
|
86
|
+
subs = discover_subcommands(help_text, model=args.model)
|
|
87
|
+
log(f"discover #{discover_calls[0]}: {subs}")
|
|
88
|
+
return subs
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
log(f"crawling `{cmd} --help` (depth={args.depth}, model={args.model})")
|
|
92
|
+
root = crawl(cmd, max_depth=args.depth, discover=_discover)
|
|
93
|
+
except FileNotFoundError as e:
|
|
94
|
+
print(f"error: {e}", file=sys.stderr)
|
|
95
|
+
return 2
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"error: crawl failed: {e}", file=sys.stderr)
|
|
98
|
+
return 3
|
|
99
|
+
|
|
100
|
+
n_nodes = len(root.flatten())
|
|
101
|
+
log(f"collected {n_nodes} help section(s) via {discover_calls[0]} discover call(s)")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
log("extracting schema")
|
|
105
|
+
schema = extract_schema(root, model=args.model, verbose=args.verbose)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"error: LLM call failed: {e}", file=sys.stderr)
|
|
108
|
+
return 3
|
|
109
|
+
|
|
110
|
+
if not schema.get("name"):
|
|
111
|
+
schema["name"] = cmd
|
|
112
|
+
|
|
113
|
+
if args.dump_schema:
|
|
114
|
+
args.dump_schema.write_text(json.dumps(schema, indent=2, ensure_ascii=False))
|
|
115
|
+
log(f"wrote schema -> {args.dump_schema}")
|
|
116
|
+
|
|
117
|
+
script = render_zsh(schema)
|
|
118
|
+
|
|
119
|
+
if not args.no_syntax_check:
|
|
120
|
+
ok, msg = _syntax_check(script)
|
|
121
|
+
if not ok:
|
|
122
|
+
print("warning: generated script failed `zsh -n` check:", file=sys.stderr)
|
|
123
|
+
print(msg, file=sys.stderr)
|
|
124
|
+
print("output still written; please review.", file=sys.stderr)
|
|
125
|
+
else:
|
|
126
|
+
log("zsh -n syntax check passed")
|
|
127
|
+
|
|
128
|
+
if args.install:
|
|
129
|
+
return _install_to_omz(cmd, script, log)
|
|
130
|
+
|
|
131
|
+
out = args.output or Path.cwd() / f"_{cmd}"
|
|
132
|
+
out.write_text(script)
|
|
133
|
+
print(f"wrote {out}")
|
|
134
|
+
print(
|
|
135
|
+
"next steps:\n"
|
|
136
|
+
f" 1. review {out}\n"
|
|
137
|
+
f" 2. move to a directory on $fpath, e.g.:\n"
|
|
138
|
+
f" mkdir -p ~/.zsh/completions && mv {out} ~/.zsh/completions/\n"
|
|
139
|
+
f" # add to ~/.zshrc if not already: fpath=(~/.zsh/completions $fpath)\n"
|
|
140
|
+
f" 3. reload completions: autoload -U compinit && compinit\n"
|
|
141
|
+
"tip: use --install to drop it straight into oh-my-zsh."
|
|
142
|
+
)
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _omz_root() -> Path:
|
|
147
|
+
return Path(os.environ.get("ZSH") or Path.home() / ".oh-my-zsh")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _install_to_omz(cmd: str, script: str, log) -> int:
|
|
151
|
+
omz = _omz_root()
|
|
152
|
+
if not omz.is_dir():
|
|
153
|
+
print(
|
|
154
|
+
f"error: oh-my-zsh not found at {omz}. "
|
|
155
|
+
"Set $ZSH or install oh-my-zsh first.",
|
|
156
|
+
file=sys.stderr,
|
|
157
|
+
)
|
|
158
|
+
return 4
|
|
159
|
+
|
|
160
|
+
plugin_dir = omz / "custom" / "plugins" / OMZ_PLUGIN_NAME
|
|
161
|
+
plugin_dir.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
target = plugin_dir / f"_{cmd}"
|
|
163
|
+
target.write_text(script)
|
|
164
|
+
log(f"installed -> {target}")
|
|
165
|
+
|
|
166
|
+
# Ensure the plugin has an entrypoint file so oh-my-zsh recognizes it.
|
|
167
|
+
stub = plugin_dir / f"{OMZ_PLUGIN_NAME}.plugin.zsh"
|
|
168
|
+
if not stub.exists():
|
|
169
|
+
stub.write_text(
|
|
170
|
+
"# Auto-generated by completion-ai.\n"
|
|
171
|
+
"# Adds this directory to fpath so the _<cmd> files here are picked up.\n"
|
|
172
|
+
"fpath=(${0:A:h} $fpath)\n"
|
|
173
|
+
)
|
|
174
|
+
log(f"created plugin stub -> {stub}")
|
|
175
|
+
|
|
176
|
+
zshrc = Path.home() / ".zshrc"
|
|
177
|
+
plugin_listed = False
|
|
178
|
+
if zshrc.exists():
|
|
179
|
+
try:
|
|
180
|
+
plugin_listed = OMZ_PLUGIN_NAME in zshrc.read_text()
|
|
181
|
+
except OSError:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
print(f"installed completion for `{cmd}` -> {target}")
|
|
185
|
+
if not plugin_listed:
|
|
186
|
+
print(
|
|
187
|
+
"\nOne more step: add the plugin to ~/.zshrc, e.g.:\n"
|
|
188
|
+
f" plugins=(... {OMZ_PLUGIN_NAME})\n"
|
|
189
|
+
)
|
|
190
|
+
print(
|
|
191
|
+
"Then reload:\n"
|
|
192
|
+
" rm -f ~/.zcompdump* && exec zsh"
|
|
193
|
+
)
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
sys.exit(main())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Recursively collect --help text for a target CLI.
|
|
2
|
+
|
|
3
|
+
Subcommand discovery is delegated to a caller-supplied callback (typically an
|
|
4
|
+
LLM call) so this module doesn't need to understand help-format dialects.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class HelpNode:
|
|
16
|
+
path: list[str]
|
|
17
|
+
help_text: str
|
|
18
|
+
children: list["HelpNode"] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
def flatten(self) -> list["HelpNode"]:
|
|
21
|
+
out = [self]
|
|
22
|
+
for c in self.children:
|
|
23
|
+
out.extend(c.flatten())
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DiscoverFn = Callable[[str], list[str]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_help(argv: list[str], timeout: float = 8.0) -> str:
|
|
31
|
+
try:
|
|
32
|
+
r = subprocess.run(
|
|
33
|
+
argv + ["--help"],
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
timeout=timeout,
|
|
37
|
+
)
|
|
38
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError) as e:
|
|
39
|
+
return f"[completion-ai: failed to run {' '.join(argv)} --help: {e}]"
|
|
40
|
+
return (r.stdout or "") + (("\n" + r.stderr) if r.stderr else "")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def crawl(cmd: str, max_depth: int, discover: DiscoverFn) -> HelpNode:
|
|
44
|
+
if shutil.which(cmd) is None:
|
|
45
|
+
raise FileNotFoundError(f"command not found on PATH: {cmd}")
|
|
46
|
+
root = HelpNode(path=[cmd], help_text=run_help([cmd]))
|
|
47
|
+
if max_depth > 1:
|
|
48
|
+
_expand(root, max_depth, 1, discover)
|
|
49
|
+
return root
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _expand(node: HelpNode, max_depth: int, depth: int, discover: DiscoverFn) -> None:
|
|
53
|
+
if depth >= max_depth:
|
|
54
|
+
return
|
|
55
|
+
for sub in discover(node.help_text):
|
|
56
|
+
if not sub or not isinstance(sub, str):
|
|
57
|
+
continue
|
|
58
|
+
child_path = node.path + [sub]
|
|
59
|
+
child = HelpNode(path=child_path, help_text=run_help(child_path))
|
|
60
|
+
node.children.append(child)
|
|
61
|
+
_expand(child, max_depth, depth + 1, discover)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Call Qwen (DashScope, OpenAI-compatible) for:
|
|
2
|
+
1. discover_subcommands - given one help text, list direct subcommand names.
|
|
3
|
+
2. extract_schema - given the full help tree, return structured CLI schema.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import textwrap
|
|
10
|
+
|
|
11
|
+
from openai import OpenAI
|
|
12
|
+
|
|
13
|
+
from .crawler import HelpNode
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
16
|
+
DEFAULT_MODEL = os.environ.get("COMPLETION_AI_MODEL", "qwen-plus")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _client() -> OpenAI:
|
|
20
|
+
api_key = os.environ.get("DASHSCOPE_API_KEY")
|
|
21
|
+
if not api_key:
|
|
22
|
+
raise RuntimeError("DASHSCOPE_API_KEY is not set")
|
|
23
|
+
base_url = os.environ.get("COMPLETION_AI_BASE_URL", DEFAULT_BASE_URL)
|
|
24
|
+
return OpenAI(api_key=api_key, base_url=base_url)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DISCOVER_SYSTEM = textwrap.dedent(
|
|
28
|
+
"""
|
|
29
|
+
You read one CLI's --help output and list its direct subcommand names.
|
|
30
|
+
|
|
31
|
+
Output JSON exactly like: {"subcommands": ["name1", "name2"]}
|
|
32
|
+
|
|
33
|
+
Rules:
|
|
34
|
+
- Only direct subcommands (not flags, not nested subcommands).
|
|
35
|
+
- For aliased entries like "plugin|plugins" return the canonical first
|
|
36
|
+
name only ("plugin").
|
|
37
|
+
- Strip placeholder tokens like "[options]", "[args]", "<target>", etc.
|
|
38
|
+
- Skip "help" if listed as a subcommand.
|
|
39
|
+
- If no subcommand section exists, return {"subcommands": []}.
|
|
40
|
+
- Be conservative: only what is explicitly listed.
|
|
41
|
+
- Do not invent commands not present in the help text.
|
|
42
|
+
"""
|
|
43
|
+
).strip()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
EXTRACT_SYSTEM = textwrap.dedent(
|
|
47
|
+
"""
|
|
48
|
+
You analyze CLI --help output and produce a JSON schema describing the
|
|
49
|
+
command tree, flags, and positional arguments. Be conservative: only
|
|
50
|
+
include things explicitly shown in the help text. Do not invent flags.
|
|
51
|
+
|
|
52
|
+
Output JSON with this exact shape (no prose, no markdown fences):
|
|
53
|
+
{
|
|
54
|
+
"name": "<root command>",
|
|
55
|
+
"description": "<one-line summary>",
|
|
56
|
+
"flags": [
|
|
57
|
+
{"long": "--foo", "short": "-f", "takes_value": true, "value_hint": "file|dir|enum|string", "choices": ["a","b"], "description": "..."}
|
|
58
|
+
],
|
|
59
|
+
"positionals": [
|
|
60
|
+
{"name": "PATH", "value_hint": "file|dir|string", "description": "...", "repeatable": false}
|
|
61
|
+
],
|
|
62
|
+
"subcommands": [
|
|
63
|
+
{ ... same shape recursively, omit fields not present ... }
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Rules:
|
|
68
|
+
- "short" may be omitted if absent. "choices" only when help lists them.
|
|
69
|
+
- "value_hint" must be one of: file, dir, command, host, user, branch,
|
|
70
|
+
string, enum, integer. Use "string" when unsure.
|
|
71
|
+
- Keep descriptions under 80 chars, single line.
|
|
72
|
+
- Omit any field you are unsure about rather than guessing.
|
|
73
|
+
- Include every subcommand whose help text appears in the input, even if
|
|
74
|
+
its own help is sparse.
|
|
75
|
+
"""
|
|
76
|
+
).strip()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def discover_subcommands(help_text: str, model: str = DEFAULT_MODEL) -> list[str]:
|
|
80
|
+
client = _client()
|
|
81
|
+
resp = client.chat.completions.create(
|
|
82
|
+
model=model,
|
|
83
|
+
messages=[
|
|
84
|
+
{"role": "system", "content": DISCOVER_SYSTEM},
|
|
85
|
+
{"role": "user", "content": help_text},
|
|
86
|
+
],
|
|
87
|
+
temperature=0,
|
|
88
|
+
response_format={"type": "json_object"},
|
|
89
|
+
)
|
|
90
|
+
data = json.loads(resp.choices[0].message.content or "{}")
|
|
91
|
+
raw = data.get("subcommands") or []
|
|
92
|
+
out: list[str] = []
|
|
93
|
+
seen: set[str] = set()
|
|
94
|
+
for s in raw:
|
|
95
|
+
if not isinstance(s, str):
|
|
96
|
+
continue
|
|
97
|
+
name = s.strip().split()[0] if s.strip() else ""
|
|
98
|
+
if name and name not in seen and not name.startswith("-"):
|
|
99
|
+
seen.add(name)
|
|
100
|
+
out.append(name)
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _serialize_tree(node: HelpNode) -> str:
|
|
105
|
+
parts = []
|
|
106
|
+
for n in node.flatten():
|
|
107
|
+
header = "$ " + " ".join(n.path) + " --help"
|
|
108
|
+
parts.append(f"{header}\n{n.help_text.rstrip()}")
|
|
109
|
+
return "\n\n---\n\n".join(parts)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def extract_schema(root: HelpNode, model: str = DEFAULT_MODEL, verbose: bool = False) -> dict:
|
|
113
|
+
client = _client()
|
|
114
|
+
user_content = (
|
|
115
|
+
f"Root command: {root.path[0]}\n\n"
|
|
116
|
+
f"Help outputs (root + subcommands):\n\n{_serialize_tree(root)}"
|
|
117
|
+
)
|
|
118
|
+
if verbose:
|
|
119
|
+
print(f"[llm] extract model={model} prompt_chars={len(user_content)}")
|
|
120
|
+
resp = client.chat.completions.create(
|
|
121
|
+
model=model,
|
|
122
|
+
messages=[
|
|
123
|
+
{"role": "system", "content": EXTRACT_SYSTEM},
|
|
124
|
+
{"role": "user", "content": user_content},
|
|
125
|
+
],
|
|
126
|
+
temperature=0.1,
|
|
127
|
+
response_format={"type": "json_object"},
|
|
128
|
+
)
|
|
129
|
+
content = resp.choices[0].message.content or "{}"
|
|
130
|
+
return json.loads(content)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Render extracted schema into a zsh _compdef completion script."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
VALUE_HINT_TO_ZSH = {
|
|
7
|
+
"file": "_files",
|
|
8
|
+
"dir": "_files -/",
|
|
9
|
+
"directory": "_files -/",
|
|
10
|
+
"command": "_command_names",
|
|
11
|
+
"host": "_hosts",
|
|
12
|
+
"user": "_users",
|
|
13
|
+
"branch": "__git_branch_names 2>/dev/null || _message branch",
|
|
14
|
+
"integer": "_message number",
|
|
15
|
+
"string": "",
|
|
16
|
+
"enum": "",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _zsh_escape(s: str) -> str:
|
|
21
|
+
if s is None:
|
|
22
|
+
return ""
|
|
23
|
+
return s.replace("\\", "\\\\").replace("'", "''").replace("[", "\\[").replace("]", "\\]").replace(":", "\\:")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _action_for(flag: dict[str, Any]) -> str:
|
|
27
|
+
if not flag.get("takes_value"):
|
|
28
|
+
return ""
|
|
29
|
+
choices = flag.get("choices") or []
|
|
30
|
+
if choices:
|
|
31
|
+
joined = " ".join(_zsh_escape(c) for c in choices)
|
|
32
|
+
return f":value:({joined})"
|
|
33
|
+
hint = (flag.get("value_hint") or "string").lower()
|
|
34
|
+
action = VALUE_HINT_TO_ZSH.get(hint, "")
|
|
35
|
+
label = _zsh_escape(hint or "value")
|
|
36
|
+
return f":{label}:{action}".rstrip(":")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _flag_spec(flag: dict[str, Any]) -> str:
|
|
40
|
+
long = flag.get("long") or ""
|
|
41
|
+
short = flag.get("short") or ""
|
|
42
|
+
desc = _zsh_escape(flag.get("description") or "")
|
|
43
|
+
action = _action_for(flag)
|
|
44
|
+
|
|
45
|
+
forms = [f for f in (short, long) if f]
|
|
46
|
+
if not forms:
|
|
47
|
+
return ""
|
|
48
|
+
if len(forms) == 2:
|
|
49
|
+
head = "{" + ",".join(forms) + "}"
|
|
50
|
+
exclusion = "(" + " ".join(forms) + ")"
|
|
51
|
+
return f"'{exclusion}'{head}'[{desc}]{action}'"
|
|
52
|
+
return f"'{forms[0]}[{desc}]{action}'"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _positional_spec(pos: dict[str, Any], idx: int) -> str:
|
|
56
|
+
name = _zsh_escape(pos.get("name") or f"arg{idx}")
|
|
57
|
+
hint = (pos.get("value_hint") or "string").lower()
|
|
58
|
+
action = VALUE_HINT_TO_ZSH.get(hint, "")
|
|
59
|
+
quantifier = "*" if pos.get("repeatable") else f"{idx}"
|
|
60
|
+
return f"'{quantifier}:{name}:{action}'".rstrip("'") + "'"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _has_subcommands(node: dict[str, Any]) -> bool:
|
|
64
|
+
return bool(node.get("subcommands"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _render_node(node: dict[str, Any], func_name: str, indent: str = " ") -> tuple[str, list[str]]:
|
|
68
|
+
"""Return (function body, list of child function definitions)."""
|
|
69
|
+
flags = node.get("flags") or []
|
|
70
|
+
positionals = node.get("positionals") or []
|
|
71
|
+
subs = node.get("subcommands") or []
|
|
72
|
+
|
|
73
|
+
lines: list[str] = []
|
|
74
|
+
lines.append(f"{func_name}() {{")
|
|
75
|
+
lines.append(f"{indent}local curcontext=$curcontext state line ret=1")
|
|
76
|
+
lines.append(f"{indent}local -a args")
|
|
77
|
+
lines.append(f"{indent}args=(")
|
|
78
|
+
|
|
79
|
+
for f in flags:
|
|
80
|
+
spec = _flag_spec(f)
|
|
81
|
+
if spec:
|
|
82
|
+
lines.append(f"{indent}{indent}{spec}")
|
|
83
|
+
|
|
84
|
+
if subs:
|
|
85
|
+
lines.append(f"{indent}{indent}'1: :->cmds'")
|
|
86
|
+
lines.append(f"{indent}{indent}'*::arg:->args'")
|
|
87
|
+
else:
|
|
88
|
+
for i, p in enumerate(positionals, start=1):
|
|
89
|
+
lines.append(f"{indent}{indent}{_positional_spec(p, i)}")
|
|
90
|
+
|
|
91
|
+
lines.append(f"{indent})")
|
|
92
|
+
lines.append(f"{indent}_arguments -C $args && ret=0")
|
|
93
|
+
|
|
94
|
+
children_defs: list[str] = []
|
|
95
|
+
if subs:
|
|
96
|
+
lines.append(f"{indent}case $state in")
|
|
97
|
+
lines.append(f"{indent}{indent}cmds)")
|
|
98
|
+
sub_descs = []
|
|
99
|
+
for s in subs:
|
|
100
|
+
sname = s.get("name") or ""
|
|
101
|
+
sdesc = _zsh_escape(s.get("description") or "")
|
|
102
|
+
if sname:
|
|
103
|
+
sub_descs.append(f"'{_zsh_escape(sname)}:{sdesc}'")
|
|
104
|
+
lines.append(f"{indent}{indent}{indent}local -a subcmds")
|
|
105
|
+
lines.append(f"{indent}{indent}{indent}subcmds=({' '.join(sub_descs)})")
|
|
106
|
+
lines.append(f"{indent}{indent}{indent}_describe 'command' subcmds && ret=0")
|
|
107
|
+
lines.append(f"{indent}{indent};;")
|
|
108
|
+
lines.append(f"{indent}{indent}args)")
|
|
109
|
+
lines.append(f"{indent}{indent}{indent}case $line[1] in")
|
|
110
|
+
for s in subs:
|
|
111
|
+
sname = s.get("name") or ""
|
|
112
|
+
if not sname:
|
|
113
|
+
continue
|
|
114
|
+
child_fn = f"{func_name}_{sname.replace('-', '_')}"
|
|
115
|
+
lines.append(f"{indent}{indent}{indent}{indent}{sname}) {child_fn} ;;")
|
|
116
|
+
body, more = _render_node(s, child_fn, indent)
|
|
117
|
+
children_defs.append(body)
|
|
118
|
+
children_defs.extend(more)
|
|
119
|
+
lines.append(f"{indent}{indent}{indent}esac")
|
|
120
|
+
lines.append(f"{indent}{indent};;")
|
|
121
|
+
lines.append(f"{indent}esac")
|
|
122
|
+
|
|
123
|
+
lines.append(f"{indent}return ret")
|
|
124
|
+
lines.append("}")
|
|
125
|
+
return "\n".join(lines), children_defs
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def render_zsh(schema: dict[str, Any]) -> str:
|
|
129
|
+
cmd = schema.get("name") or "cmd"
|
|
130
|
+
safe_cmd = cmd.replace("-", "_")
|
|
131
|
+
root_fn = f"_{safe_cmd}"
|
|
132
|
+
body, children = _render_node(schema, root_fn)
|
|
133
|
+
|
|
134
|
+
header = (
|
|
135
|
+
f"#compdef {cmd}\n"
|
|
136
|
+
f"# Generated by completion-ai. Review before installing.\n"
|
|
137
|
+
f"# Move to a directory on $fpath (e.g. ~/.zsh/completions/_{cmd}),\n"
|
|
138
|
+
f"# then run: autoload -U compinit && compinit\n"
|
|
139
|
+
)
|
|
140
|
+
parts = [header, body, *children, f"{root_fn} \"$@\""]
|
|
141
|
+
return "\n\n".join(parts) + "\n"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "completion-ai"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "LLM-powered zsh completion scaffolding for arbitrary CLIs"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "yangwb", email = "binyang617@gmail.com" }]
|
|
9
|
+
keywords = ["cli", "completion", "zsh", "llm", "qwen", "dashscope"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: MacOS",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development",
|
|
22
|
+
"Topic :: System :: Shells",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"openai>=1.40.0",
|
|
27
|
+
"httpx[socks]>=0.27.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Yangeyu/completion-ai"
|
|
32
|
+
Repository = "https://github.com/Yangeyu/completion-ai"
|
|
33
|
+
Issues = "https://github.com/Yangeyu/completion-ai/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
completion-ai = "completion_ai.cli:main"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["hatchling"]
|
|
40
|
+
build-backend = "hatchling.build"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["completion_ai"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.sdist]
|
|
46
|
+
include = [
|
|
47
|
+
"completion_ai",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE",
|
|
50
|
+
"pyproject.toml",
|
|
51
|
+
]
|