agent-tool-shared-cli 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.
- agent_tool_shared_cli-0.1.0/PKG-INFO +120 -0
- agent_tool_shared_cli-0.1.0/README.md +95 -0
- agent_tool_shared_cli-0.1.0/pyproject.toml +56 -0
- agent_tool_shared_cli-0.1.0/setup.cfg +4 -0
- agent_tool_shared_cli-0.1.0/src/agent_tool_shared_cli.egg-info/PKG-INFO +120 -0
- agent_tool_shared_cli-0.1.0/src/agent_tool_shared_cli.egg-info/SOURCES.txt +16 -0
- agent_tool_shared_cli-0.1.0/src/agent_tool_shared_cli.egg-info/dependency_links.txt +1 -0
- agent_tool_shared_cli-0.1.0/src/agent_tool_shared_cli.egg-info/requires.txt +6 -0
- agent_tool_shared_cli-0.1.0/src/agent_tool_shared_cli.egg-info/top_level.txt +1 -0
- agent_tool_shared_cli-0.1.0/src/agentcli/__init__.py +58 -0
- agent_tool_shared_cli-0.1.0/src/agentcli/appspec.py +96 -0
- agent_tool_shared_cli-0.1.0/src/agentcli/credentials.py +145 -0
- agent_tool_shared_cli-0.1.0/src/agentcli/errors.py +93 -0
- agent_tool_shared_cli-0.1.0/src/agentcli/output.py +288 -0
- agent_tool_shared_cli-0.1.0/tests/test_appspec.py +97 -0
- agent_tool_shared_cli-0.1.0/tests/test_contract.py +135 -0
- agent_tool_shared_cli-0.1.0/tests/test_credentials.py +129 -0
- agent_tool_shared_cli-0.1.0/tests/test_output_render.py +187 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-tool-shared-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared chassis for agent-ready CLIs: the exit-code contract, JSON/table/markdown/CSV output with field projection, and keyring-backed credentials.
|
|
5
|
+
Author: Zierhut IT, Alexander Zierhut
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/alexander-zierhut/agent-tool-shared-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/alexander-zierhut/agent-tool-shared-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/alexander-zierhut/agent-tool-shared-cli/issues
|
|
10
|
+
Keywords: cli,command-line,ai-agent,llm,automation,json-output,keyring,exit-codes,agent-ready
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: keyring>=25
|
|
21
|
+
Requires-Dist: rich>=13
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-timeout>=2.3; extra == "test"
|
|
25
|
+
|
|
26
|
+
# agent-tool-shared-cli
|
|
27
|
+
|
|
28
|
+
The shared chassis for the **`agent-tool-<x>-cli`** family — agent-ready command-line
|
|
29
|
+
tools that an LLM can drive with no prior knowledge of them.
|
|
30
|
+
|
|
31
|
+
Consumers: [`agent-tool-openproject-cli`](https://github.com/alexander-zierhut/agent-tool-openproject-cli),
|
|
32
|
+
`agent-tool-drone-cli` (in progress).
|
|
33
|
+
|
|
34
|
+
This repo also holds **[BLUEPRINT.md](BLUEPRINT.md)** — the engineering standard the
|
|
35
|
+
whole family is built to. Read that first if you are standing up a new tool.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install agent-tool-shared-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What's in it
|
|
42
|
+
|
|
43
|
+
This package is deliberately small. It holds **the agent contract** and the pure
|
|
44
|
+
utilities that implement it — the things that must be *identical* across every tool,
|
|
45
|
+
because an agent learns the contract once and applies it everywhere.
|
|
46
|
+
|
|
47
|
+
| Module | What |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `agentcli.errors` | The exit-code taxonomy (`0/1/3/4/5/6/7/130`) + `DryRun` |
|
|
50
|
+
| `agentcli.output` | `Emitter`: json/table/markdown/csv, `--fields` projection, NDJSON streaming |
|
|
51
|
+
| `agentcli.credentials` | Keyring storage with a `0600` fallback and an env override |
|
|
52
|
+
| `agentcli.appspec` | `AppSpec` — the two strings that make all of the above tool-specific |
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from agentcli import AppSpec, Credentials, Emitter, OutputFormat, NotFoundError
|
|
56
|
+
|
|
57
|
+
SPEC = AppSpec(name="drone-cli", env_prefix="DRONECLI")
|
|
58
|
+
|
|
59
|
+
SPEC.config_dir() # ~/.config/drone-cli (or $DRONECLI_CONFIG_DIR)
|
|
60
|
+
SPEC.env("TOKEN") # "DRONECLI_TOKEN"
|
|
61
|
+
|
|
62
|
+
Credentials(SPEC).get_token("default") # env > keyring > 0600 file
|
|
63
|
+
Emitter(OutputFormat.json, fields=["id", "status"]).emit(rows)
|
|
64
|
+
raise NotFoundError("no such build") # -> exit 5, JSON on stderr
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## The exit-code contract
|
|
68
|
+
|
|
69
|
+
Published API. You may leave a code unallocated, or repurpose one deliberately.
|
|
70
|
+
**You may never renumber one** — agents branch on these, and they are documented in
|
|
71
|
+
three places per tool (README, the in-binary `guide`, and the Claude skill).
|
|
72
|
+
|
|
73
|
+
| Code | Meaning |
|
|
74
|
+
|---|---|
|
|
75
|
+
| 0 | success (including a successful `--dry-run`) |
|
|
76
|
+
| 1 | generic error |
|
|
77
|
+
| 2 | *reserved for Click/Typer usage errors — never allocate* |
|
|
78
|
+
| 3 | config error |
|
|
79
|
+
| 4 | auth error (401/403) |
|
|
80
|
+
| 5 | not found (404) |
|
|
81
|
+
| 6 | conflict (409 / optimistic locking) |
|
|
82
|
+
| 7 | validation error (422) — see `fieldErrors` |
|
|
83
|
+
| 8+ | per-tool, and only for a condition you have **observed** |
|
|
84
|
+
| 130 | SIGINT |
|
|
85
|
+
|
|
86
|
+
## What is deliberately NOT here
|
|
87
|
+
|
|
88
|
+
`client.py`, `serialize.py`, `resolve.py` and the domain commands stay in each tool.
|
|
89
|
+
They *look* shareable and are not:
|
|
90
|
+
|
|
91
|
+
> OpenProject stops paginating on an authoritative `total` — "never stop on a short
|
|
92
|
+
> page". Drone has no total, and a short page **is** the terminator. The rule that
|
|
93
|
+
> inverts between tool #1 and tool #2 is exactly the rule you must not hoist.
|
|
94
|
+
|
|
95
|
+
Auth schemes, retry matrices and error-body shapes are the same story. Pulling them in
|
|
96
|
+
would make this a framework with a config object per tool, which is how shared-code
|
|
97
|
+
projects die.
|
|
98
|
+
|
|
99
|
+
**Share the contract, not the transport.**
|
|
100
|
+
|
|
101
|
+
## Contributing
|
|
102
|
+
|
|
103
|
+
You need nothing but Python:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install -e '.[test]'
|
|
107
|
+
pytest # 74 tests, no network, no services
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Changes here affect every downstream CLI. Two rules:
|
|
111
|
+
|
|
112
|
+
1. **Never renumber an exit code, and never change an output shape** without treating
|
|
113
|
+
it as a breaking change — downstream agents depend on both.
|
|
114
|
+
2. **Don't add a module because two tools happen to share it today.** Wait until a
|
|
115
|
+
third does, and until you can state the rule it obeys. `tests/test_contract.py`
|
|
116
|
+
is the tripwire: if a change makes you edit it, stop and think.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# agent-tool-shared-cli
|
|
2
|
+
|
|
3
|
+
The shared chassis for the **`agent-tool-<x>-cli`** family — agent-ready command-line
|
|
4
|
+
tools that an LLM can drive with no prior knowledge of them.
|
|
5
|
+
|
|
6
|
+
Consumers: [`agent-tool-openproject-cli`](https://github.com/alexander-zierhut/agent-tool-openproject-cli),
|
|
7
|
+
`agent-tool-drone-cli` (in progress).
|
|
8
|
+
|
|
9
|
+
This repo also holds **[BLUEPRINT.md](BLUEPRINT.md)** — the engineering standard the
|
|
10
|
+
whole family is built to. Read that first if you are standing up a new tool.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install agent-tool-shared-cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What's in it
|
|
17
|
+
|
|
18
|
+
This package is deliberately small. It holds **the agent contract** and the pure
|
|
19
|
+
utilities that implement it — the things that must be *identical* across every tool,
|
|
20
|
+
because an agent learns the contract once and applies it everywhere.
|
|
21
|
+
|
|
22
|
+
| Module | What |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `agentcli.errors` | The exit-code taxonomy (`0/1/3/4/5/6/7/130`) + `DryRun` |
|
|
25
|
+
| `agentcli.output` | `Emitter`: json/table/markdown/csv, `--fields` projection, NDJSON streaming |
|
|
26
|
+
| `agentcli.credentials` | Keyring storage with a `0600` fallback and an env override |
|
|
27
|
+
| `agentcli.appspec` | `AppSpec` — the two strings that make all of the above tool-specific |
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from agentcli import AppSpec, Credentials, Emitter, OutputFormat, NotFoundError
|
|
31
|
+
|
|
32
|
+
SPEC = AppSpec(name="drone-cli", env_prefix="DRONECLI")
|
|
33
|
+
|
|
34
|
+
SPEC.config_dir() # ~/.config/drone-cli (or $DRONECLI_CONFIG_DIR)
|
|
35
|
+
SPEC.env("TOKEN") # "DRONECLI_TOKEN"
|
|
36
|
+
|
|
37
|
+
Credentials(SPEC).get_token("default") # env > keyring > 0600 file
|
|
38
|
+
Emitter(OutputFormat.json, fields=["id", "status"]).emit(rows)
|
|
39
|
+
raise NotFoundError("no such build") # -> exit 5, JSON on stderr
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## The exit-code contract
|
|
43
|
+
|
|
44
|
+
Published API. You may leave a code unallocated, or repurpose one deliberately.
|
|
45
|
+
**You may never renumber one** — agents branch on these, and they are documented in
|
|
46
|
+
three places per tool (README, the in-binary `guide`, and the Claude skill).
|
|
47
|
+
|
|
48
|
+
| Code | Meaning |
|
|
49
|
+
|---|---|
|
|
50
|
+
| 0 | success (including a successful `--dry-run`) |
|
|
51
|
+
| 1 | generic error |
|
|
52
|
+
| 2 | *reserved for Click/Typer usage errors — never allocate* |
|
|
53
|
+
| 3 | config error |
|
|
54
|
+
| 4 | auth error (401/403) |
|
|
55
|
+
| 5 | not found (404) |
|
|
56
|
+
| 6 | conflict (409 / optimistic locking) |
|
|
57
|
+
| 7 | validation error (422) — see `fieldErrors` |
|
|
58
|
+
| 8+ | per-tool, and only for a condition you have **observed** |
|
|
59
|
+
| 130 | SIGINT |
|
|
60
|
+
|
|
61
|
+
## What is deliberately NOT here
|
|
62
|
+
|
|
63
|
+
`client.py`, `serialize.py`, `resolve.py` and the domain commands stay in each tool.
|
|
64
|
+
They *look* shareable and are not:
|
|
65
|
+
|
|
66
|
+
> OpenProject stops paginating on an authoritative `total` — "never stop on a short
|
|
67
|
+
> page". Drone has no total, and a short page **is** the terminator. The rule that
|
|
68
|
+
> inverts between tool #1 and tool #2 is exactly the rule you must not hoist.
|
|
69
|
+
|
|
70
|
+
Auth schemes, retry matrices and error-body shapes are the same story. Pulling them in
|
|
71
|
+
would make this a framework with a config object per tool, which is how shared-code
|
|
72
|
+
projects die.
|
|
73
|
+
|
|
74
|
+
**Share the contract, not the transport.**
|
|
75
|
+
|
|
76
|
+
## Contributing
|
|
77
|
+
|
|
78
|
+
You need nothing but Python:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install -e '.[test]'
|
|
82
|
+
pytest # 74 tests, no network, no services
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Changes here affect every downstream CLI. Two rules:
|
|
86
|
+
|
|
87
|
+
1. **Never renumber an exit code, and never change an output shape** without treating
|
|
88
|
+
it as a breaking change — downstream agents depend on both.
|
|
89
|
+
2. **Don't add a module because two tools happen to share it today.** Wait until a
|
|
90
|
+
third does, and until you can state the rule it obeys. `tests/test_contract.py`
|
|
91
|
+
is the tripwire: if a change makes you edit it, stop and think.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-tool-shared-cli"
|
|
7
|
+
description = "Shared chassis for agent-ready CLIs: the exit-code contract, JSON/table/markdown/CSV output with field projection, and keyring-backed credentials."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Zierhut IT" }, { name = "Alexander Zierhut" }]
|
|
12
|
+
dynamic = ["version"]
|
|
13
|
+
keywords = [
|
|
14
|
+
"cli", "command-line", "ai-agent", "llm", "automation",
|
|
15
|
+
"json-output", "keyring", "exit-codes", "agent-ready",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Topic :: Utilities",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"keyring>=25",
|
|
28
|
+
"rich>=13",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
test = [
|
|
33
|
+
"pytest>=8",
|
|
34
|
+
"pytest-timeout>=2.3",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/alexander-zierhut/agent-tool-shared-cli"
|
|
39
|
+
Repository = "https://github.com/alexander-zierhut/agent-tool-shared-cli"
|
|
40
|
+
Issues = "https://github.com/alexander-zierhut/agent-tool-shared-cli/issues"
|
|
41
|
+
|
|
42
|
+
# No [project.scripts]: this is a library, not a command.
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
# Single source of truth for the version — the scar opcli carries (it duplicates
|
|
48
|
+
# the number in pyproject.toml AND __init__.py, so a bump can ship a wheel that
|
|
49
|
+
# reports the wrong version in its User-Agent and in the skill it writes).
|
|
50
|
+
[tool.setuptools.dynamic]
|
|
51
|
+
version = { attr = "agentcli.__version__" }
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
addopts = "-ra -q"
|
|
56
|
+
timeout = 60
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-tool-shared-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared chassis for agent-ready CLIs: the exit-code contract, JSON/table/markdown/CSV output with field projection, and keyring-backed credentials.
|
|
5
|
+
Author: Zierhut IT, Alexander Zierhut
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/alexander-zierhut/agent-tool-shared-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/alexander-zierhut/agent-tool-shared-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/alexander-zierhut/agent-tool-shared-cli/issues
|
|
10
|
+
Keywords: cli,command-line,ai-agent,llm,automation,json-output,keyring,exit-codes,agent-ready
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: keyring>=25
|
|
21
|
+
Requires-Dist: rich>=13
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-timeout>=2.3; extra == "test"
|
|
25
|
+
|
|
26
|
+
# agent-tool-shared-cli
|
|
27
|
+
|
|
28
|
+
The shared chassis for the **`agent-tool-<x>-cli`** family — agent-ready command-line
|
|
29
|
+
tools that an LLM can drive with no prior knowledge of them.
|
|
30
|
+
|
|
31
|
+
Consumers: [`agent-tool-openproject-cli`](https://github.com/alexander-zierhut/agent-tool-openproject-cli),
|
|
32
|
+
`agent-tool-drone-cli` (in progress).
|
|
33
|
+
|
|
34
|
+
This repo also holds **[BLUEPRINT.md](BLUEPRINT.md)** — the engineering standard the
|
|
35
|
+
whole family is built to. Read that first if you are standing up a new tool.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install agent-tool-shared-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What's in it
|
|
42
|
+
|
|
43
|
+
This package is deliberately small. It holds **the agent contract** and the pure
|
|
44
|
+
utilities that implement it — the things that must be *identical* across every tool,
|
|
45
|
+
because an agent learns the contract once and applies it everywhere.
|
|
46
|
+
|
|
47
|
+
| Module | What |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `agentcli.errors` | The exit-code taxonomy (`0/1/3/4/5/6/7/130`) + `DryRun` |
|
|
50
|
+
| `agentcli.output` | `Emitter`: json/table/markdown/csv, `--fields` projection, NDJSON streaming |
|
|
51
|
+
| `agentcli.credentials` | Keyring storage with a `0600` fallback and an env override |
|
|
52
|
+
| `agentcli.appspec` | `AppSpec` — the two strings that make all of the above tool-specific |
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from agentcli import AppSpec, Credentials, Emitter, OutputFormat, NotFoundError
|
|
56
|
+
|
|
57
|
+
SPEC = AppSpec(name="drone-cli", env_prefix="DRONECLI")
|
|
58
|
+
|
|
59
|
+
SPEC.config_dir() # ~/.config/drone-cli (or $DRONECLI_CONFIG_DIR)
|
|
60
|
+
SPEC.env("TOKEN") # "DRONECLI_TOKEN"
|
|
61
|
+
|
|
62
|
+
Credentials(SPEC).get_token("default") # env > keyring > 0600 file
|
|
63
|
+
Emitter(OutputFormat.json, fields=["id", "status"]).emit(rows)
|
|
64
|
+
raise NotFoundError("no such build") # -> exit 5, JSON on stderr
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## The exit-code contract
|
|
68
|
+
|
|
69
|
+
Published API. You may leave a code unallocated, or repurpose one deliberately.
|
|
70
|
+
**You may never renumber one** — agents branch on these, and they are documented in
|
|
71
|
+
three places per tool (README, the in-binary `guide`, and the Claude skill).
|
|
72
|
+
|
|
73
|
+
| Code | Meaning |
|
|
74
|
+
|---|---|
|
|
75
|
+
| 0 | success (including a successful `--dry-run`) |
|
|
76
|
+
| 1 | generic error |
|
|
77
|
+
| 2 | *reserved for Click/Typer usage errors — never allocate* |
|
|
78
|
+
| 3 | config error |
|
|
79
|
+
| 4 | auth error (401/403) |
|
|
80
|
+
| 5 | not found (404) |
|
|
81
|
+
| 6 | conflict (409 / optimistic locking) |
|
|
82
|
+
| 7 | validation error (422) — see `fieldErrors` |
|
|
83
|
+
| 8+ | per-tool, and only for a condition you have **observed** |
|
|
84
|
+
| 130 | SIGINT |
|
|
85
|
+
|
|
86
|
+
## What is deliberately NOT here
|
|
87
|
+
|
|
88
|
+
`client.py`, `serialize.py`, `resolve.py` and the domain commands stay in each tool.
|
|
89
|
+
They *look* shareable and are not:
|
|
90
|
+
|
|
91
|
+
> OpenProject stops paginating on an authoritative `total` — "never stop on a short
|
|
92
|
+
> page". Drone has no total, and a short page **is** the terminator. The rule that
|
|
93
|
+
> inverts between tool #1 and tool #2 is exactly the rule you must not hoist.
|
|
94
|
+
|
|
95
|
+
Auth schemes, retry matrices and error-body shapes are the same story. Pulling them in
|
|
96
|
+
would make this a framework with a config object per tool, which is how shared-code
|
|
97
|
+
projects die.
|
|
98
|
+
|
|
99
|
+
**Share the contract, not the transport.**
|
|
100
|
+
|
|
101
|
+
## Contributing
|
|
102
|
+
|
|
103
|
+
You need nothing but Python:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install -e '.[test]'
|
|
107
|
+
pytest # 74 tests, no network, no services
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Changes here affect every downstream CLI. Two rules:
|
|
111
|
+
|
|
112
|
+
1. **Never renumber an exit code, and never change an output shape** without treating
|
|
113
|
+
it as a breaking change — downstream agents depend on both.
|
|
114
|
+
2. **Don't add a module because two tools happen to share it today.** Wait until a
|
|
115
|
+
third does, and until you can state the rule it obeys. `tests/test_contract.py`
|
|
116
|
+
is the tripwire: if a change makes you edit it, stop and think.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/agent_tool_shared_cli.egg-info/PKG-INFO
|
|
4
|
+
src/agent_tool_shared_cli.egg-info/SOURCES.txt
|
|
5
|
+
src/agent_tool_shared_cli.egg-info/dependency_links.txt
|
|
6
|
+
src/agent_tool_shared_cli.egg-info/requires.txt
|
|
7
|
+
src/agent_tool_shared_cli.egg-info/top_level.txt
|
|
8
|
+
src/agentcli/__init__.py
|
|
9
|
+
src/agentcli/appspec.py
|
|
10
|
+
src/agentcli/credentials.py
|
|
11
|
+
src/agentcli/errors.py
|
|
12
|
+
src/agentcli/output.py
|
|
13
|
+
tests/test_appspec.py
|
|
14
|
+
tests/test_contract.py
|
|
15
|
+
tests/test_credentials.py
|
|
16
|
+
tests/test_output_render.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentcli
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""agentcli — the shared chassis for `agent-tool-<x>-cli` tools.
|
|
2
|
+
|
|
3
|
+
This package is deliberately small. It holds **the agent contract** and the pure
|
|
4
|
+
utilities that implement it — the things that must be *identical* across every
|
|
5
|
+
tool we ship, because an agent learns the contract once and applies it everywhere:
|
|
6
|
+
|
|
7
|
+
* :mod:`agentcli.errors` — the exit-code taxonomy (0/1/3/4/5/6/7/130) + ``DryRun``
|
|
8
|
+
* :mod:`agentcli.output` — json/table/markdown/csv, ``--fields`` projection, NDJSON
|
|
9
|
+
* :mod:`agentcli.credentials` — keyring with a 0600 fallback and an env override
|
|
10
|
+
* :mod:`agentcli.appspec` — the two strings that make the above tool-specific
|
|
11
|
+
|
|
12
|
+
**What is deliberately NOT here**, and why it matters more than what is:
|
|
13
|
+
|
|
14
|
+
``client.py``, ``serialize.py``, ``resolve.py`` and the domain command modules
|
|
15
|
+
stay in each tool. They *look* shareable and are not — OpenProject's pagination
|
|
16
|
+
stops on an authoritative ``total`` ("never stop on a short page"), while Drone
|
|
17
|
+
has no total and a short page IS the terminator. The rule that inverts between
|
|
18
|
+
tool #1 and tool #2 is exactly the rule you must not hoist. Auth schemes, retry
|
|
19
|
+
matrices and error-body shapes are the same story.
|
|
20
|
+
|
|
21
|
+
Share the contract, not the transport.
|
|
22
|
+
|
|
23
|
+
Pulling those in would turn this into a framework with a config object per tool,
|
|
24
|
+
which is how shared-code projects die.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .appspec import AppSpec
|
|
28
|
+
from .credentials import Credentials
|
|
29
|
+
from .errors import (
|
|
30
|
+
ApiError,
|
|
31
|
+
AuthError,
|
|
32
|
+
ConfigError,
|
|
33
|
+
ConflictError,
|
|
34
|
+
DryRun,
|
|
35
|
+
NotFoundError,
|
|
36
|
+
OpError,
|
|
37
|
+
ValidationError,
|
|
38
|
+
)
|
|
39
|
+
from .output import Emitter, OutputFormat, print_error # NDJSON is Emitter.stream_json()
|
|
40
|
+
|
|
41
|
+
__version__ = "0.1.0"
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"AppSpec",
|
|
45
|
+
"Credentials",
|
|
46
|
+
"Emitter",
|
|
47
|
+
"OutputFormat",
|
|
48
|
+
"print_error",
|
|
49
|
+
"OpError",
|
|
50
|
+
"ApiError",
|
|
51
|
+
"AuthError",
|
|
52
|
+
"ConfigError",
|
|
53
|
+
"ConflictError",
|
|
54
|
+
"NotFoundError",
|
|
55
|
+
"ValidationError",
|
|
56
|
+
"DryRun",
|
|
57
|
+
"__version__",
|
|
58
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""`AppSpec` — the two strings that make a shared chassis tool-specific.
|
|
2
|
+
|
|
3
|
+
Every tool in the `agent-tool-<x>-cli` family needs the same four things from
|
|
4
|
+
its identity:
|
|
5
|
+
|
|
6
|
+
* a **config directory** ``~/.config/<name>/``
|
|
7
|
+
* a **keyring service name** ``<name>``
|
|
8
|
+
* an **env-var namespace** ``<PREFIX>_TOKEN``, ``<PREFIX>_CONFIG_DIR``, …
|
|
9
|
+
* a **relocatable config dir**, so tests are hermetic
|
|
10
|
+
|
|
11
|
+
So that is all `AppSpec` carries. It is deliberately not a plugin system, a
|
|
12
|
+
registry or a settings framework — two strings and a few pure functions.
|
|
13
|
+
|
|
14
|
+
Why this exists at all: in `opcli` the config-dir logic was **duplicated** in
|
|
15
|
+
both ``config.py`` and ``credentials.py``. Two copies of "where do I live?" that
|
|
16
|
+
nothing forced to agree — relocate one and the token and the profile end up in
|
|
17
|
+
different directories. Here there is exactly one.
|
|
18
|
+
|
|
19
|
+
SPEC = AppSpec(name="op-cli", env_prefix="OPCLI")
|
|
20
|
+
SPEC.config_dir() # -> ~/.config/op-cli (or $OPCLI_CONFIG_DIR)
|
|
21
|
+
SPEC.env("TOKEN") # -> "OPCLI_TOKEN"
|
|
22
|
+
SPEC.getenv("BASE_URL") # -> os.environ.get("OPCLI_BASE_URL")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class AppSpec:
|
|
34
|
+
"""Identity of one CLI. Frozen: this is configuration, not state."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
"""Directory + keyring service name, e.g. ``op-cli``, ``drone-cli``."""
|
|
38
|
+
|
|
39
|
+
env_prefix: str
|
|
40
|
+
"""Env-var namespace WITHOUT the trailing underscore, e.g. ``OPCLI``."""
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
# These two are the whole contract; a typo here silently relocates a
|
|
44
|
+
# user's config or splits their token from their profile.
|
|
45
|
+
if not self.name or "/" in self.name:
|
|
46
|
+
raise ValueError(f"AppSpec.name must be a bare directory name, got {self.name!r}")
|
|
47
|
+
if not self.env_prefix or not self.env_prefix.isupper():
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"AppSpec.env_prefix must be UPPERCASE and non-empty, got {self.env_prefix!r}"
|
|
50
|
+
)
|
|
51
|
+
if self.env_prefix.endswith("_"):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"AppSpec.env_prefix must not end with '_' (it is added for you), got {self.env_prefix!r}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# ---- env ---------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def env(self, suffix: str) -> str:
|
|
59
|
+
"""The full env-var name for *suffix*: ``env("TOKEN") -> "OPCLI_TOKEN"``."""
|
|
60
|
+
return f"{self.env_prefix}_{suffix}"
|
|
61
|
+
|
|
62
|
+
def getenv(self, suffix: str, default: str | None = None) -> str | None:
|
|
63
|
+
"""Read ``<PREFIX>_<SUFFIX>`` from the environment."""
|
|
64
|
+
return os.environ.get(self.env(suffix), default)
|
|
65
|
+
|
|
66
|
+
# ---- paths -------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def config_dir(self) -> Path:
|
|
69
|
+
"""Where this tool's config lives.
|
|
70
|
+
|
|
71
|
+
A **function, not a module constant** — and that single property is what
|
|
72
|
+
makes the test suites hermetic. As a constant it would freeze at import
|
|
73
|
+
time, before a test could point ``<PREFIX>_CONFIG_DIR`` at a tmpdir, and
|
|
74
|
+
every test run would read and write the developer's real config.
|
|
75
|
+
|
|
76
|
+
Precedence: ``<PREFIX>_CONFIG_DIR`` > ``XDG_CONFIG_HOME``/<name> > ``~/.config/<name>``.
|
|
77
|
+
"""
|
|
78
|
+
base = self.getenv("CONFIG_DIR")
|
|
79
|
+
if base:
|
|
80
|
+
return Path(base)
|
|
81
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
82
|
+
root = Path(xdg) if xdg else Path.home() / ".config"
|
|
83
|
+
return root / self.name
|
|
84
|
+
|
|
85
|
+
def config_file(self) -> Path:
|
|
86
|
+
return self.config_dir() / "config.json"
|
|
87
|
+
|
|
88
|
+
def credentials_file(self) -> Path:
|
|
89
|
+
"""The 0600 fallback used only when no OS keyring is available."""
|
|
90
|
+
return self.config_dir() / "credentials.json"
|
|
91
|
+
|
|
92
|
+
# ---- keyring -----------------------------------------------------
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def keyring_service(self) -> str:
|
|
96
|
+
return self.name
|