humancli 0.1.0__py3-none-any.whl

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.
agentcli/_wizard.py ADDED
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from enum import Enum
5
+ from typing import Any, Literal, get_origin
6
+
7
+ from . import prompt as prompt_api
8
+ from ._types import CommandSchema, MISSING, ParameterSpec
9
+
10
+
11
+ def wizard_schema(command: CommandSchema) -> dict[str, Any]:
12
+ return {
13
+ "command": command.command,
14
+ "description": command.description,
15
+ "parameters": [
16
+ serialize_parameter(parameter)
17
+ for parameter in command.all_parameters
18
+ if not parameter.hidden
19
+ ],
20
+ }
21
+
22
+
23
+ def prompt_for_parameter(
24
+ parameter: ParameterSpec, *, stdin: Any = None, stdout: Any = None
25
+ ) -> Any:
26
+ message = parameter.prompt or parameter.description or parameter.name
27
+ if parameter.is_bool:
28
+ default = (
29
+ bool(parameter.default)
30
+ if parameter.has_default and parameter.default is not None
31
+ else False
32
+ )
33
+ return prompt_api.confirm(message, default=default, stdin=stdin, stdout=stdout)
34
+ if parameter.choices:
35
+ return prompt_api.select(
36
+ message,
37
+ parameter.choices,
38
+ default=_default(parameter.default),
39
+ stdin=stdin,
40
+ stdout=stdout,
41
+ )
42
+ if parameter.secret:
43
+ return prompt_api.password(
44
+ message, default=_default(parameter.default), stdin=stdin, stdout=stdout
45
+ )
46
+ if parameter.is_list:
47
+ raw = prompt_api.text(message, default="", stdin=stdin, stdout=stdout)
48
+ return [item.strip() for item in raw.split(",") if item.strip()]
49
+ return prompt_api.text(
50
+ message, default=_default(parameter.default), stdin=stdin, stdout=stdout
51
+ )
52
+
53
+
54
+ def serialize_parameter(parameter: ParameterSpec) -> dict[str, Any]:
55
+ return {
56
+ "name": parameter.name,
57
+ "kind": parameter.kind,
58
+ "flag": None if parameter.kind == "argument" else f"--{parameter.cli_name}",
59
+ "required": parameter.required,
60
+ "description": parameter.description,
61
+ "choices": list(parameter.choices) if parameter.choices else None,
62
+ "default": None
63
+ if parameter.default in (MISSING, inspect.Signature.empty)
64
+ else parameter.default,
65
+ "type": parameter_type(parameter.annotation),
66
+ }
67
+
68
+
69
+ def parameter_type(annotation: Any) -> str:
70
+ origin = get_origin(annotation)
71
+ if origin is Literal:
72
+ return "literal"
73
+ if origin in (list, tuple):
74
+ return "list"
75
+ if inspect.isclass(annotation) and issubclass(annotation, Enum):
76
+ return "enum"
77
+ return getattr(annotation, "__name__", str(annotation))
78
+
79
+
80
+ def _default(value: Any) -> str | None:
81
+ if value in (None, MISSING, inspect.Signature.empty):
82
+ return None
83
+ return str(value)
agentcli/prompt.py ADDED
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import getpass
4
+ import sys
5
+ from typing import Any
6
+
7
+
8
+ def text(
9
+ message: str,
10
+ *,
11
+ default: Any = None,
12
+ stdin: Any = None,
13
+ stdout: Any = None,
14
+ ) -> str:
15
+ input_stream = stdin or sys.stdin
16
+ output_stream = stdout or sys.stdout
17
+ suffix = f" [{default}]" if default not in (None, "") else ""
18
+ output_stream.write(f"? {message}{suffix}: ")
19
+ output_stream.flush()
20
+ value = input_stream.readline().rstrip("\n")
21
+ return str(default) if value == "" and default is not None else value
22
+
23
+
24
+ def password(message: str, *, stdin: Any = None, stdout: Any = None) -> str:
25
+ input_stream = stdin or sys.stdin
26
+ output_stream = stdout or sys.stdout
27
+ if input_stream is sys.stdin and output_stream is sys.stdout:
28
+ return getpass.getpass(f"? {message}: ")
29
+ output_stream.write(f"? {message}: ")
30
+ output_stream.flush()
31
+ return input_stream.readline().rstrip("\n")
32
+
33
+
34
+ def confirm(
35
+ message: str,
36
+ *,
37
+ default: bool = False,
38
+ stdin: Any = None,
39
+ stdout: Any = None,
40
+ ) -> bool:
41
+ answer = (
42
+ text(
43
+ f"{message} [{'Y/n' if default else 'y/N'}]",
44
+ default="y" if default else "n",
45
+ stdin=stdin,
46
+ stdout=stdout,
47
+ )
48
+ .strip()
49
+ .lower()
50
+ )
51
+ return answer in {"y", "yes", "true", "1"}
52
+
53
+
54
+ def select(
55
+ message: str,
56
+ options: list[str],
57
+ *,
58
+ stdin: Any = None,
59
+ stdout: Any = None,
60
+ ) -> str:
61
+ output_stream = stdout or sys.stdout
62
+ output_stream.write(f"? {message}: {', '.join(options)}\n")
63
+ output_stream.flush()
64
+ choice = text("Choice", stdin=stdin, stdout=stdout)
65
+ if choice in options:
66
+ return choice
67
+ if choice.isdigit():
68
+ index = int(choice) - 1
69
+ if 0 <= index < len(options):
70
+ return options[index]
71
+ raise ValueError(f"Invalid selection: {choice}")
agentcli/py.typed ADDED
File without changes
agentcli/testing.py ADDED
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import json
5
+ from dataclasses import dataclass
6
+ from typing import Any, Mapping
7
+
8
+
9
+ @dataclass
10
+ class CliResult:
11
+ exit_code: int
12
+ output: str
13
+ data: Any = None
14
+ error: Any = None
15
+
16
+
17
+ class CliRunner:
18
+ def __init__(self, app: Any) -> None:
19
+ self.app = app
20
+
21
+ def invoke(
22
+ self,
23
+ argv: list[str],
24
+ *,
25
+ env: Mapping[str, str] | None = None,
26
+ is_tty: bool = False,
27
+ stdin: str = "",
28
+ ) -> CliResult:
29
+ stdout = io.StringIO()
30
+ input_stream = io.StringIO(stdin)
31
+ result = self.app.invoke(
32
+ argv=argv,
33
+ env=dict(env or {}),
34
+ is_tty=is_tty,
35
+ stdin=input_stream,
36
+ stdout=stdout,
37
+ )
38
+ output = stdout.getvalue()
39
+ data = result.data
40
+ error = result.error
41
+ try:
42
+ parsed = json.loads(output)
43
+ except Exception:
44
+ parsed = None
45
+ if isinstance(parsed, dict) and "ok" in parsed:
46
+ data = parsed.get("data")
47
+ error = parsed.get("error")
48
+ elif parsed is not None:
49
+ data = parsed
50
+ return CliResult(
51
+ exit_code=result.exit_code,
52
+ output=output,
53
+ data=data,
54
+ error=error,
55
+ )
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: humancli
3
+ Version: 0.1.0
4
+ Summary: Python CLIs for agents and humans
5
+ Project-URL: Repository, https://github.com/elyase/agentcli
6
+ Project-URL: Issues, https://github.com/elyase/agentcli/issues
7
+ Author-email: Yaser Martinez Palenzuela <yaser.martinez@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agent,ai,cli,command-line,llm
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: docstring-parser>=0.16
23
+ Provides-Extra: all
24
+ Requires-Dist: mcp>=1.0; extra == 'all'
25
+ Requires-Dist: pydantic>=2.0; extra == 'all'
26
+ Requires-Dist: pyyaml>=6.0; extra == 'all'
27
+ Requires-Dist: rich>=13.0; extra == 'all'
28
+ Provides-Extra: mcp
29
+ Requires-Dist: mcp>=1.0; extra == 'mcp'
30
+ Provides-Extra: pydantic
31
+ Requires-Dist: pydantic>=2.0; extra == 'pydantic'
32
+ Provides-Extra: rich
33
+ Requires-Dist: rich>=13.0; extra == 'rich'
34
+ Provides-Extra: yaml
35
+ Requires-Dist: pyyaml>=6.0; extra == 'yaml'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # agentcli
39
+
40
+ Python CLIs for agents and humans.
41
+
42
+ agentcli builds command-line interfaces that produce structured, parseable output for AI agents while remaining human-friendly. Type hints are the schema — a function signature IS the CLI specification.
43
+
44
+ ## Install
45
+
46
+ ```sh
47
+ pip install humancli
48
+ ```
49
+
50
+ The package installs as `humancli` on PyPI but you import it as `agentcli`:
51
+
52
+ ```python
53
+ from agentcli import App
54
+ ```
55
+
56
+ ## Quick start
57
+
58
+ ### Single function
59
+
60
+ ```python
61
+ from agentcli import run
62
+
63
+ def greet(name: str):
64
+ """Greet someone."""
65
+ return {"message": f"hello {name}"}
66
+
67
+ run(greet)
68
+ ```
69
+
70
+ ```sh
71
+ $ greet world
72
+ message: hello world
73
+
74
+ $ greet world --json
75
+ {"ok": true, "data": {"message": "hello world"}}
76
+
77
+ $ greet --llms
78
+ # greet
79
+ | Command | Description |
80
+ |---------|-------------|
81
+ | `greet <name>` | Greet someone |
82
+ ```
83
+
84
+ ### Multi-command app
85
+
86
+ ```python
87
+ from agentcli import App
88
+
89
+ app = App("my-cli", version="1.0.0")
90
+
91
+ @app.command
92
+ def status():
93
+ """Show status."""
94
+ return {"clean": True, "branch": "main"}
95
+
96
+ @app.command
97
+ def install(package: str, *, save_dev: bool = False):
98
+ """Install a package."""
99
+ return {"added": 1, "packages": 451}
100
+
101
+ app()
102
+ ```
103
+
104
+ Parameters before `*` are positional arguments. Parameters after `*` are named options/flags. This is just Python's own syntax.
105
+
106
+ ### Parameter metadata
107
+
108
+ ```python
109
+ from typing import Annotated, Literal
110
+ from agentcli import App, Param
111
+
112
+ app = App("deploy-cli")
113
+
114
+ @app.command
115
+ def deploy(
116
+ env: Annotated[Literal["staging", "prod"], Param(help="Target environment")],
117
+ *,
118
+ token: Annotated[str, Param(env="DEPLOY_TOKEN", secret=True)] = "",
119
+ ):
120
+ """Deploy to an environment."""
121
+ return {"url": f"https://{env}.example.com"}
122
+
123
+ app()
124
+ ```
125
+
126
+ ### Sub-apps
127
+
128
+ ```python
129
+ app = App("gh")
130
+ pr = App("pr")
131
+
132
+ @pr.command
133
+ def list_(*, state: Literal["open", "closed"] = "open"):
134
+ """List pull requests."""
135
+ return {"prs": [], "state": state}
136
+
137
+ app.mount(pr)
138
+ app()
139
+
140
+ # $ gh pr list --state closed
141
+ ```
142
+
143
+ ### Default commands
144
+
145
+ ```python
146
+ app = App("fetch")
147
+
148
+ @app.default
149
+ def fetch_cases(*, limit: int = 20):
150
+ """Fetch cases."""
151
+ return {"fetched": limit}
152
+
153
+ # Runs when no sub-command is given:
154
+ # $ fetch --limit 5
155
+ ```
156
+
157
+ ## Agent discovery
158
+
159
+ Every agentcli app gets built-in flags for agent consumption:
160
+
161
+ - `--llms` — markdown command index
162
+ - `--llms-full` — full JSON schema of all commands
163
+ - `--json` / `--yaml` / `--jsonl` — structured output formats
164
+ - `--mcp` — start as an MCP server (requires `agentcli[mcp]`)
165
+
166
+ ## Optional extras
167
+
168
+ ```sh
169
+ pip install humancli[rich] # rich terminal formatting
170
+ pip install humancli[pydantic] # pydantic model support
171
+ pip install humancli[yaml] # yaml output format
172
+ pip install humancli[mcp] # MCP server mode
173
+ pip install humancli[all] # everything
174
+ ```
175
+
176
+ ## License
177
+
178
+ MIT
@@ -0,0 +1,18 @@
1
+ agentcli/__init__.py,sha256=HxjImv5iEFR6gnh5QyoEne6sGKbiRevyWBd40z1aufA,403
2
+ agentcli/_agents.py,sha256=17m1RXYH2eAKioNrT9Gj8ahQjXZc7gyOzrGn_MPrlUE,3095
3
+ agentcli/_app.py,sha256=298niScGWCQ4GHw4y7v5Jfx2oI3xnaWJ6JfMLHvtpsw,15006
4
+ agentcli/_context.py,sha256=SqMmYndkVHLVKIDyH64Jes3GZfmlVASY3EtO18PGbRY,1161
5
+ agentcli/_errors.py,sha256=cEmYSCs76Agp-NSqEI7UFdij9HYjMWPInEyQvYoHfFo,3564
6
+ agentcli/_help.py,sha256=HhlN__lKUSB9T4VV_EGPxtsRnF8CrFJawRm7ttefjB0,2214
7
+ agentcli/_output.py,sha256=8FLwRjeQjHtZ2dRUcP8WS64w6bv_Fqdg0_ioUiYPqek,8371
8
+ agentcli/_parser.py,sha256=zuz2r0Bed0ro3rwOZPAaf4CJm2jYNjmMMHLnwAUtLhA,13822
9
+ agentcli/_schema.py,sha256=nPzBZHB0TuML1i397OyKcMz1jEiMFhwLCqJf5dNiv9M,4708
10
+ agentcli/_types.py,sha256=OzBDnAu8eyR6Uw3tAVV2UNKdtmUyky20eg1MYC5K7sk,3126
11
+ agentcli/_wizard.py,sha256=WRML8rKLHfOjw50xW9j6ZPNx_LaGK-a-3ergnjolCp4,2725
12
+ agentcli/prompt.py,sha256=AGBBSeGbjY7qiIxUMXledz_vLpnkwdb22RwBuvUY2T4,1880
13
+ agentcli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ agentcli/testing.py,sha256=wm6oZeYRhbHx-Da0sIvmtKl6AB_LZgOGhrh-4LLaiw0,1303
15
+ humancli-0.1.0.dist-info/METADATA,sha256=vR0V8-81rzlduSyUcK38lWptC9GNRs87YKyZmRwLGW8,4210
16
+ humancli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
17
+ humancli-0.1.0.dist-info/licenses/LICENSE,sha256=71w9pR4vtroGYKlGefIRpDZmUXAWVZIyyJpljlFZOzo,1082
18
+ humancli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yaser Martinez Palenzuela
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.