acli-spec 0.1.4__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.
@@ -0,0 +1,38 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ *.egg
7
+ dist/
8
+ build/
9
+ .eggs/
10
+ *.whl
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # Testing / coverage
18
+ .coverage
19
+ htmlcov/
20
+ .pytest_cache/
21
+ .mypy_cache/
22
+
23
+ # IDE
24
+ .idea/
25
+ .vscode/
26
+ *.swp
27
+ *.swo
28
+ *~
29
+
30
+ # OS
31
+ .DS_Store
32
+ Thumbs.db
33
+
34
+ # ACLI generated
35
+ .cli/
36
+
37
+ # MkDocs
38
+ site/
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: acli-spec
3
+ Version: 0.1.4
4
+ Summary: Python SDK for the ACLI (Agent-friendly CLI) specification
5
+ Project-URL: Homepage, https://github.com/alpibrusl/acli
6
+ Project-URL: Documentation, https://alpibrusl.github.io/acli
7
+ Project-URL: Repository, https://github.com/alpibrusl/acli
8
+ Project-URL: Issues, https://github.com/alpibrusl/acli/issues
9
+ Project-URL: Changelog, https://github.com/alpibrusl/acli/blob/main/CHANGELOG.md
10
+ Author: ACLI Contributors
11
+ License-Expression: EUPL-1.2
12
+ Keywords: agent,ai,cli,specification
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: typer>=0.9.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.10; extra == 'dev'
26
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.4; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # acli-spec
32
+
33
+ Python SDK for the [ACLI (Agent-friendly CLI) specification](../../ACLI_SPEC.md).
34
+
35
+ Build CLI tools that AI agents can discover, learn, and use autonomously.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install acli-spec
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```python
46
+ from pathlib import Path
47
+ from acli import ACLIApp, acli_command, OutputFormat
48
+ import typer
49
+
50
+ app = ACLIApp(name="myapp", version="1.0.0")
51
+
52
+ @app.command()
53
+ @acli_command(
54
+ examples=[
55
+ ("Run a task", "myapp run --file task.yaml"),
56
+ ("Dry-run a task", "myapp run --file task.yaml --dry-run"),
57
+ ],
58
+ idempotent=False,
59
+ )
60
+ def run(
61
+ file: Path = typer.Option(..., help="Path to task file. type:path"),
62
+ dry_run: bool = typer.Option(False, help="Preview without executing."),
63
+ output: OutputFormat = typer.Option(OutputFormat.text, help="Output format."),
64
+ ) -> None:
65
+ """Execute a task from a YAML file."""
66
+ ...
67
+
68
+ if __name__ == "__main__":
69
+ app.run()
70
+ ```
71
+
72
+ ## What you get automatically
73
+
74
+ - `introspect` command with full command tree as JSON
75
+ - `.cli/` folder generation (README, examples, schemas)
76
+ - JSON error envelope on `--output json`
77
+ - Semantic exit codes (0-9)
78
+ - `--version` with semver output
79
+
80
+ ## License
81
+
82
+ [EUPL-1.2](../../LICENSE)
@@ -0,0 +1,52 @@
1
+ # acli-spec
2
+
3
+ Python SDK for the [ACLI (Agent-friendly CLI) specification](../../ACLI_SPEC.md).
4
+
5
+ Build CLI tools that AI agents can discover, learn, and use autonomously.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install acli-spec
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from pathlib import Path
17
+ from acli import ACLIApp, acli_command, OutputFormat
18
+ import typer
19
+
20
+ app = ACLIApp(name="myapp", version="1.0.0")
21
+
22
+ @app.command()
23
+ @acli_command(
24
+ examples=[
25
+ ("Run a task", "myapp run --file task.yaml"),
26
+ ("Dry-run a task", "myapp run --file task.yaml --dry-run"),
27
+ ],
28
+ idempotent=False,
29
+ )
30
+ def run(
31
+ file: Path = typer.Option(..., help="Path to task file. type:path"),
32
+ dry_run: bool = typer.Option(False, help="Preview without executing."),
33
+ output: OutputFormat = typer.Option(OutputFormat.text, help="Output format."),
34
+ ) -> None:
35
+ """Execute a task from a YAML file."""
36
+ ...
37
+
38
+ if __name__ == "__main__":
39
+ app.run()
40
+ ```
41
+
42
+ ## What you get automatically
43
+
44
+ - `introspect` command with full command tree as JSON
45
+ - `.cli/` folder generation (README, examples, schemas)
46
+ - JSON error envelope on `--output json`
47
+ - Semantic exit codes (0-9)
48
+ - `--version` with semver output
49
+
50
+ ## License
51
+
52
+ [EUPL-1.2](../../LICENSE)
@@ -0,0 +1,117 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "acli-spec"
7
+ version = "0.1.4"
8
+ description = "Python SDK for the ACLI (Agent-friendly CLI) specification"
9
+ readme = "README.md"
10
+ license = "EUPL-1.2"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "ACLI Contributors" }]
13
+ keywords = ["cli", "agent", "ai", "specification"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "typer>=0.9.0",
27
+ ]
28
+
29
+ [project.scripts]
30
+ acli = "acli.cli:main"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/alpibrusl/acli"
34
+ Documentation = "https://alpibrusl.github.io/acli"
35
+ Repository = "https://github.com/alpibrusl/acli"
36
+ Issues = "https://github.com/alpibrusl/acli/issues"
37
+ Changelog = "https://github.com/alpibrusl/acli/blob/main/CHANGELOG.md"
38
+
39
+ [project.optional-dependencies]
40
+ dev = [
41
+ "pytest>=8.0",
42
+ "pytest-cov>=5.0",
43
+ "ruff>=0.4",
44
+ "mypy>=1.10",
45
+ ]
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/acli"]
49
+
50
+ # ── Ruff ──────────────────────────────────────────────────────────────────────
51
+
52
+ [tool.ruff]
53
+ target-version = "py310"
54
+ line-length = 99
55
+
56
+ [tool.ruff.lint]
57
+ select = [
58
+ "F", # pyflakes
59
+ "E", "W", # pycodestyle
60
+ "I", # isort
61
+ "N", # pep8-naming
62
+ "UP", # pyupgrade
63
+ "B", # flake8-bugbear
64
+ "A", # flake8-builtins
65
+ "SIM", # flake8-simplify
66
+ "T20", # flake8-print
67
+ "PT", # flake8-pytest-style
68
+ "RUF", # ruff-specific
69
+ "S", # flake8-bandit (security)
70
+ "C4", # flake8-comprehensions
71
+ "DTZ", # flake8-datetimez
72
+ "PIE", # flake8-pie
73
+ "RET", # flake8-return
74
+ "TCH", # flake8-type-checking
75
+ "ARG", # flake8-unused-arguments
76
+ "PLC", "PLE", "PLW", # pylint
77
+ ]
78
+ ignore = [
79
+ "S101", # allow assert in tests
80
+ "BLE001", # allow broad exception catches (needed for introspect resilience)
81
+ "B008", # typer.Option() in defaults is standard Typer pattern
82
+ ]
83
+
84
+ [tool.ruff.lint.per-file-ignores]
85
+ "tests/**/*.py" = ["ARG001", "ARG002", "ARG005", "S", "T20", "PLC0415", "TC003", "PT017", "SIM105"]
86
+
87
+ [tool.ruff.lint.isort]
88
+ known-first-party = ["acli"]
89
+
90
+ # ── Mypy ──────────────────────────────────────────────────────────────────────
91
+
92
+ [tool.mypy]
93
+ python_version = "3.10"
94
+ strict = true
95
+ warn_return_any = true
96
+ warn_unused_configs = true
97
+ disallow_untyped_defs = true
98
+
99
+ [[tool.mypy.overrides]]
100
+ module = ["typer", "typer.*"]
101
+ ignore_missing_imports = true
102
+
103
+ [[tool.mypy.overrides]]
104
+ module = ["acli.cli"]
105
+ disallow_untyped_decorators = false
106
+
107
+ # ── Pytest ────────────────────────────────────────────────────────────────────
108
+
109
+ [tool.pytest.ini_options]
110
+ testpaths = ["tests"]
111
+ addopts = [
112
+ "--strict-markers",
113
+ "--cov=acli",
114
+ "--cov-report=term-missing",
115
+ "--cov-report=html:htmlcov",
116
+ "--cov-fail-under=90",
117
+ ]
@@ -0,0 +1,36 @@
1
+ """ACLI — Agent-friendly CLI Python SDK."""
2
+
3
+ from acli.app import ACLIApp
4
+ from acli.command import CommandExample, CommandMeta, acli_command
5
+ from acli.errors import (
6
+ ACLIError,
7
+ ConflictError,
8
+ InvalidArgsError,
9
+ NotFoundError,
10
+ PreconditionError,
11
+ suggest_flag,
12
+ )
13
+ from acli.exit_codes import ExitCode
14
+ from acli.output import OutputFormat, emit, error_envelope, success_envelope
15
+ from acli.skill import generate_skill
16
+
17
+ __all__ = [
18
+ "ACLIApp",
19
+ "ACLIError",
20
+ "CommandExample",
21
+ "CommandMeta",
22
+ "ConflictError",
23
+ "ExitCode",
24
+ "InvalidArgsError",
25
+ "NotFoundError",
26
+ "OutputFormat",
27
+ "PreconditionError",
28
+ "acli_command",
29
+ "emit",
30
+ "error_envelope",
31
+ "generate_skill",
32
+ "success_envelope",
33
+ "suggest_flag",
34
+ ]
35
+
36
+ __version__ = "0.1.4"
@@ -0,0 +1,179 @@
1
+ """ACLIApp — the main application class wrapping Typer per ACLI spec §8."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import typer
11
+
12
+ from acli.cli_folder import generate_cli_folder, needs_update
13
+ from acli.errors import ACLIError
14
+ from acli.exit_codes import ExitCode
15
+ from acli.introspect import build_command_tree
16
+ from acli.output import OutputFormat, emit, error_envelope, success_envelope
17
+ from acli.skill import generate_skill
18
+
19
+
20
+ class ACLIApp:
21
+ """ACLI-compliant application wrapper around Typer.
22
+
23
+ Automatically registers the ``introspect`` command, enforces JSON error
24
+ envelopes, and generates the ``.cli/`` folder.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ name: str,
30
+ version: str,
31
+ *,
32
+ cli_dir: Path | None = None,
33
+ **typer_kwargs: Any,
34
+ ) -> None:
35
+ self.name = name
36
+ self.version = version
37
+ self.cli_dir = cli_dir
38
+ self._typer = typer.Typer(name=name, help=typer_kwargs.pop("help", None), **typer_kwargs)
39
+ self._register_introspect()
40
+ self._register_version()
41
+ self._register_skill()
42
+
43
+ @property
44
+ def typer_app(self) -> typer.Typer:
45
+ """Access the underlying Typer instance."""
46
+ return self._typer
47
+
48
+ # ── Public API ────────────────────────────────────────────────────────────
49
+
50
+ def command(self, *args: Any, **kwargs: Any) -> Any:
51
+ """Register a command — proxy to typer.command()."""
52
+ return self._typer.command(*args, **kwargs)
53
+
54
+ def add_typer(self, *args: Any, **kwargs: Any) -> None:
55
+ """Add a sub-group — proxy to typer.add_typer()."""
56
+ self._typer.add_typer(*args, **kwargs)
57
+
58
+ def run(self) -> None:
59
+ """Run the application with ACLI error handling."""
60
+ try:
61
+ self._typer()
62
+ except ACLIError as exc:
63
+ self._handle_acli_error(exc)
64
+ except SystemExit:
65
+ raise
66
+ except Exception as exc:
67
+ self._handle_unexpected_error(exc)
68
+
69
+ def get_command_tree(self) -> dict[str, Any]:
70
+ """Build the introspection command tree."""
71
+ return build_command_tree(self._typer, self.name, self.version)
72
+
73
+ # ── Built-in commands ─────────────────────────────────────────────────────
74
+
75
+ def _register_introspect(self) -> None:
76
+ @self._typer.command(name="introspect", hidden=True)
77
+ def introspect(
78
+ acli_version: bool = typer.Option(
79
+ False, "--acli-version", help="Show only the ACLI spec version. type:bool"
80
+ ),
81
+ output: OutputFormat = typer.Option(
82
+ OutputFormat.json, "--output", help="Output format. type:enum[text|json|table]"
83
+ ),
84
+ ) -> None:
85
+ """Output the full command tree as JSON for agent consumption."""
86
+ if acli_version:
87
+ if output == OutputFormat.json:
88
+ json.dump({"acli_version": "0.1.0"}, sys.stdout)
89
+ sys.stdout.write("\n")
90
+ else:
91
+ sys.stdout.write("acli 0.1.0\n")
92
+ return
93
+
94
+ tree = self.get_command_tree()
95
+
96
+ # Update .cli/ if needed
97
+ if needs_update(tree, self.cli_dir):
98
+ generate_cli_folder(tree, self.cli_dir)
99
+
100
+ emit(success_envelope("introspect", tree, version=self.version), output)
101
+
102
+ def _register_version(self) -> None:
103
+ @self._typer.command(name="version", hidden=True)
104
+ def version_cmd(
105
+ output: OutputFormat = typer.Option(
106
+ OutputFormat.text,
107
+ "--output",
108
+ help="Output format. type:enum[text|json|table]",
109
+ ),
110
+ ) -> None:
111
+ """Show version information."""
112
+ if output == OutputFormat.json:
113
+ data = {
114
+ "tool": self.name,
115
+ "version": self.version,
116
+ "acli_version": "0.1.0",
117
+ }
118
+ emit(success_envelope("version", data, version=self.version), output)
119
+ else:
120
+ sys.stdout.write(f"{self.name} {self.version}\n")
121
+ sys.stdout.write("acli 0.1.0\n")
122
+
123
+ # Update .cli/ if needed
124
+ tree = self.get_command_tree()
125
+ if needs_update(tree, self.cli_dir):
126
+ generate_cli_folder(tree, self.cli_dir)
127
+
128
+ def _register_skill(self) -> None:
129
+ @self._typer.command(name="skill", hidden=True)
130
+ def skill_cmd(
131
+ out: str = typer.Option(
132
+ "",
133
+ "--out",
134
+ help="Write skill file to this path instead of stdout. type:path",
135
+ ),
136
+ output: OutputFormat = typer.Option(
137
+ OutputFormat.text,
138
+ "--output",
139
+ help="Output format. type:enum[text|json|table]",
140
+ ),
141
+ ) -> None:
142
+ """Generate a SKILLS.md file for agent bootstrapping."""
143
+ tree = self.get_command_tree()
144
+ target = Path(out) if out else None
145
+ content = generate_skill(tree, target_path=target)
146
+
147
+ if output == OutputFormat.json:
148
+ data = {"path": str(target) if target else None, "content": content}
149
+ emit(success_envelope("skill", data, version=self.version), output)
150
+ elif target:
151
+ sys.stdout.write(f"Skill file written to {target}\n")
152
+ else:
153
+ sys.stdout.write(content)
154
+
155
+ # ── Error handling ────────────────────────────────────────────────────────
156
+
157
+ def _handle_acli_error(self, exc: ACLIError) -> None:
158
+ cmd_name = exc.command or self.name
159
+ envelope = error_envelope(
160
+ cmd_name,
161
+ code=exc.code.name,
162
+ message=str(exc),
163
+ hint=exc.hint,
164
+ docs=exc.docs,
165
+ version=self.version,
166
+ )
167
+ emit(envelope, OutputFormat.json)
168
+ raise SystemExit(exc.code.value)
169
+
170
+ def _handle_unexpected_error(self, exc: Exception) -> None:
171
+ envelope = error_envelope(
172
+ self.name,
173
+ code="GENERAL_ERROR",
174
+ message=str(exc),
175
+ hint="This is an unexpected error. Please report it.",
176
+ version=self.version,
177
+ )
178
+ emit(envelope, OutputFormat.json)
179
+ raise SystemExit(ExitCode.GENERAL_ERROR)