lekha-poth-cli 2.1.0__tar.gz → 2.3.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.
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/PKG-INFO +36 -10
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/README.md +35 -9
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/pyproject.toml +1 -1
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/scripts/generate_spec.py +110 -7
- lekha_poth_cli-2.3.0/skills/lp-cli/README.md +105 -0
- lekha_poth_cli-2.3.0/skills/lp-cli/SKILL.md +410 -0
- lekha_poth_cli-2.3.0/skills/lp-cli/evals/evals.json +188 -0
- lekha_poth_cli-2.3.0/skills/lp-cli/evals/seed_fixtures.sh +93 -0
- lekha_poth_cli-2.3.0/skills/lp-cli/evals/trigger-evals.json +82 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/admin.md +73 -69
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/auth-account.md +83 -47
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/chats.md +6 -6
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/legacy.md +25 -25
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/library.md +37 -37
- lekha_poth_cli-2.3.0/skills/lp-cli/references/manifest.md +89 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/public-content.md +18 -16
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/vocab.md +49 -26
- lekha_poth_cli-2.3.0/skills/lp-cli/references/workflows.md +183 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/scripts/generate_references.py +121 -27
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/__init__.py +1 -1
- lekha_poth_cli-2.3.0/src/lekha_poth_cli/app.py +662 -0
- lekha_poth_cli-2.3.0/src/lekha_poth_cli/auth_flow.py +296 -0
- lekha_poth_cli-2.3.0/src/lekha_poth_cli/client.py +235 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/config.py +62 -4
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/endpoints.json +76 -49
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/errors.py +49 -0
- lekha_poth_cli-2.3.0/src/lekha_poth_cli/media.py +313 -0
- lekha_poth_cli-2.3.0/src/lekha_poth_cli/publish.py +480 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/register.py +50 -11
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/spec.py +6 -1
- lekha_poth_cli-2.3.0/tests/test_auth.py +254 -0
- lekha_poth_cli-2.3.0/tests/test_config.py +65 -0
- lekha_poth_cli-2.3.0/tests/test_contract_version_parity.py +34 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/tests/test_errors.py +77 -1
- lekha_poth_cli-2.3.0/tests/test_media.py +265 -0
- lekha_poth_cli-2.3.0/tests/test_publish.py +373 -0
- lekha_poth_cli-2.3.0/tests/test_setup.py +154 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/tests/test_spec_fields.py +161 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/uv.lock +117 -119
- lekha_poth_cli-2.1.0/skills/lp-cli/SKILL.md +0 -340
- lekha_poth_cli-2.1.0/skills/lp-cli/evals/evals.json +0 -108
- lekha_poth_cli-2.1.0/src/lekha_poth_cli/app.py +0 -287
- lekha_poth_cli-2.1.0/src/lekha_poth_cli/client.py +0 -149
- lekha_poth_cli-2.1.0/src/lekha_poth_cli/media.py +0 -94
- lekha_poth_cli-2.1.0/tests/test_config.py +0 -28
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/.gitignore +0 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/scripts/generate_catalog_v2.py +0 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/__main__.py +0 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/output.py +0 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/tests/test_catalog_v2.py +0 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/tests/test_coverage.py +0 -0
- {lekha_poth_cli-2.1.0 → lekha_poth_cli-2.3.0}/tests/test_references_current.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lekha-poth-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Production Typer CLI covering every Lekha Poth HTTP API endpoint.
|
|
5
5
|
Author: Hermes Agent
|
|
6
6
|
License: MIT
|
|
@@ -17,23 +17,49 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
# Lekha Poth CLI (`lp`) v2
|
|
18
18
|
|
|
19
19
|
Production Typer + Pydantic client for **every** Lekha Poth HTTP endpoint,
|
|
20
|
-
generated from `backend/src/app/api` on `main` (kept in sync —
|
|
21
|
-
|
|
20
|
+
generated from `backend/src/app/api` on `main` (kept in sync — `lp endpoints`
|
|
21
|
+
for the live count, synced at product v0.20.1 / `bf5ea4e`; regenerate with
|
|
22
22
|
`generate_spec.py` after any route change, guarded in CI by
|
|
23
23
|
`backend/tests/unit/test_agent_spec_current.py`).
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
It replaced the stdlib `lp.py` / `lp_media.py`, which were removed in #389.
|
|
26
26
|
|
|
27
27
|
## Install
|
|
28
28
|
|
|
29
|
+
Published on PyPI as [`lekha-poth-cli`](https://pypi.org/project/lekha-poth-cli/).
|
|
30
|
+
No checkout needed:
|
|
31
|
+
|
|
29
32
|
```bash
|
|
30
|
-
uv
|
|
31
|
-
uv
|
|
32
|
-
|
|
33
|
-
lp --help
|
|
33
|
+
uv tool install lekha-poth-cli # → $HOME/.local/bin/lp
|
|
34
|
+
uv tool upgrade lekha-poth-cli # later, to pick up new routes and flags
|
|
35
|
+
$HOME/.local/bin/lp --version
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
On macOS `/usr/bin/lp` is the print spooler and is on `PATH`, so call the
|
|
39
|
+
Lekha Poth CLI by its absolute path (`$HOME/.local/bin/lp`), or use the
|
|
40
|
+
alias entry point `lekha-poth`, which nothing else claims. To try it without
|
|
41
|
+
installing: `uvx --from lekha-poth-cli lp --help`.
|
|
42
|
+
|
|
43
|
+
An **agent skill** that teaches an LLM agent to drive this CLI ships in the
|
|
44
|
+
sdist under `skills/lp-cli/` (and in the repository at
|
|
45
|
+
`scripts/agent/lpcli/skills/lp-cli/`); its `README.md` says where to put it.
|
|
46
|
+
|
|
47
|
+
### Developing against a checkout
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd scripts/agent/lpcli
|
|
51
|
+
uv venv .venv && uv pip install --python .venv/bin/python -e ".[dev]"
|
|
52
|
+
.venv/bin/python -m lekha_poth_cli --version
|
|
53
|
+
# or, as a tool that tracks the working tree: uv tool install -e .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Releasing
|
|
57
|
+
|
|
58
|
+
**Actions → Release (lekha-poth-cli) → Run workflow** (`bump` minor by default,
|
|
59
|
+
or an exact `version`). It stamps every copy of the contract version
|
|
60
|
+
(`scripts/ci/stamp-lpcli-version.py`), runs the tests, `uv build`s, commits
|
|
61
|
+
and tags `lpcli-vX.Y.Z` on `main`, then `uv publish`es to PyPI. See
|
|
62
|
+
`docs/CICD.md` → "Releasing the agent CLI".
|
|
37
63
|
|
|
38
64
|
## Configure (once)
|
|
39
65
|
|
|
@@ -42,7 +68,7 @@ lp config set --api-base "$LEKHA_POTH_API_BASE" \
|
|
|
42
68
|
--site-url "$LEKHA_POTH_SITE_URL" \
|
|
43
69
|
--api-key lpak_…
|
|
44
70
|
# hosted production example:
|
|
45
|
-
# --api-base https://api.lekhapoth.com/api/v1 --site-url https://lekhapoth.com
|
|
71
|
+
# --api-base https://api.lekhapoth.com/api/v1 --site-url https://www.lekhapoth.com
|
|
46
72
|
lp config set-defaults --output pretty --default-size 20
|
|
47
73
|
lp status
|
|
48
74
|
lp whoami
|
|
@@ -1,23 +1,49 @@
|
|
|
1
1
|
# Lekha Poth CLI (`lp`) v2
|
|
2
2
|
|
|
3
3
|
Production Typer + Pydantic client for **every** Lekha Poth HTTP endpoint,
|
|
4
|
-
generated from `backend/src/app/api` on `main` (kept in sync —
|
|
5
|
-
|
|
4
|
+
generated from `backend/src/app/api` on `main` (kept in sync — `lp endpoints`
|
|
5
|
+
for the live count, synced at product v0.20.1 / `bf5ea4e`; regenerate with
|
|
6
6
|
`generate_spec.py` after any route change, guarded in CI by
|
|
7
7
|
`backend/tests/unit/test_agent_spec_current.py`).
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
It replaced the stdlib `lp.py` / `lp_media.py`, which were removed in #389.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
+
Published on PyPI as [`lekha-poth-cli`](https://pypi.org/project/lekha-poth-cli/).
|
|
14
|
+
No checkout needed:
|
|
15
|
+
|
|
13
16
|
```bash
|
|
14
|
-
uv
|
|
15
|
-
uv
|
|
16
|
-
|
|
17
|
-
lp --help
|
|
17
|
+
uv tool install lekha-poth-cli # → $HOME/.local/bin/lp
|
|
18
|
+
uv tool upgrade lekha-poth-cli # later, to pick up new routes and flags
|
|
19
|
+
$HOME/.local/bin/lp --version
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
On macOS `/usr/bin/lp` is the print spooler and is on `PATH`, so call the
|
|
23
|
+
Lekha Poth CLI by its absolute path (`$HOME/.local/bin/lp`), or use the
|
|
24
|
+
alias entry point `lekha-poth`, which nothing else claims. To try it without
|
|
25
|
+
installing: `uvx --from lekha-poth-cli lp --help`.
|
|
26
|
+
|
|
27
|
+
An **agent skill** that teaches an LLM agent to drive this CLI ships in the
|
|
28
|
+
sdist under `skills/lp-cli/` (and in the repository at
|
|
29
|
+
`scripts/agent/lpcli/skills/lp-cli/`); its `README.md` says where to put it.
|
|
30
|
+
|
|
31
|
+
### Developing against a checkout
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd scripts/agent/lpcli
|
|
35
|
+
uv venv .venv && uv pip install --python .venv/bin/python -e ".[dev]"
|
|
36
|
+
.venv/bin/python -m lekha_poth_cli --version
|
|
37
|
+
# or, as a tool that tracks the working tree: uv tool install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Releasing
|
|
41
|
+
|
|
42
|
+
**Actions → Release (lekha-poth-cli) → Run workflow** (`bump` minor by default,
|
|
43
|
+
or an exact `version`). It stamps every copy of the contract version
|
|
44
|
+
(`scripts/ci/stamp-lpcli-version.py`), runs the tests, `uv build`s, commits
|
|
45
|
+
and tags `lpcli-vX.Y.Z` on `main`, then `uv publish`es to PyPI. See
|
|
46
|
+
`docs/CICD.md` → "Releasing the agent CLI".
|
|
21
47
|
|
|
22
48
|
## Configure (once)
|
|
23
49
|
|
|
@@ -26,7 +52,7 @@ lp config set --api-base "$LEKHA_POTH_API_BASE" \
|
|
|
26
52
|
--site-url "$LEKHA_POTH_SITE_URL" \
|
|
27
53
|
--api-key lpak_…
|
|
28
54
|
# hosted production example:
|
|
29
|
-
# --api-base https://api.lekhapoth.com/api/v1 --site-url https://lekhapoth.com
|
|
55
|
+
# --api-base https://api.lekhapoth.com/api/v1 --site-url https://www.lekhapoth.com
|
|
30
56
|
lp config set-defaults --output pretty --default-size 20
|
|
31
57
|
lp status
|
|
32
58
|
lp whoami
|
|
@@ -55,6 +55,8 @@ import uuid as _uuid
|
|
|
55
55
|
from pathlib import Path
|
|
56
56
|
from typing import Any
|
|
57
57
|
|
|
58
|
+
from pydantic import AwareDatetime, FutureDatetime, NaiveDatetime, PastDatetime
|
|
59
|
+
|
|
58
60
|
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
59
61
|
OUT = REPO_ROOT / "scripts" / "agent" / "lpcli" / "src" / "lekha_poth_cli" / "endpoints.json"
|
|
60
62
|
API_V1_PREFIX = "/api/v1"
|
|
@@ -62,6 +64,10 @@ API_V1_PREFIX = "/api/v1"
|
|
|
62
64
|
#: Routes that are infrastructure, not API surface.
|
|
63
65
|
SKIP_PATHS = {"/docs", "/openapi.json", "/redoc", "/metrics"}
|
|
64
66
|
|
|
67
|
+
#: Service-to-service routes (#381): the Cloudflare jobs Worker's endpoint.
|
|
68
|
+
#: Not for agents or any user credential, so never catalogued.
|
|
69
|
+
SKIP_PREFIXES = ("/api/v1/internal/",)
|
|
70
|
+
|
|
65
71
|
|
|
66
72
|
def _ensure_backend_importable() -> None:
|
|
67
73
|
src = REPO_ROOT / "backend" / "src"
|
|
@@ -105,6 +111,15 @@ _SCALARS: dict[Any, str] = {
|
|
|
105
111
|
bool: "bool",
|
|
106
112
|
_uuid.UUID: "uuid",
|
|
107
113
|
_dt.datetime: "datetime",
|
|
114
|
+
# Pydantic's constrained datetimes are distinct classes at runtime, not
|
|
115
|
+
# `Annotated[datetime, ...]`, so nothing above would unwrap them and the
|
|
116
|
+
# field would degrade to `str`. `publishedAt` is `AwareDatetime` — the API
|
|
117
|
+
# refuses a timezone-less value — and the CLI must still call it a
|
|
118
|
+
# datetime.
|
|
119
|
+
AwareDatetime: "datetime",
|
|
120
|
+
NaiveDatetime: "datetime",
|
|
121
|
+
PastDatetime: "datetime",
|
|
122
|
+
FutureDatetime: "datetime",
|
|
108
123
|
_dt.date: "date",
|
|
109
124
|
}
|
|
110
125
|
|
|
@@ -122,9 +137,27 @@ def _cli_type(annotation: Any) -> str:
|
|
|
122
137
|
if annotation in (dict, Any) or origin is dict:
|
|
123
138
|
return "json"
|
|
124
139
|
if origin in (list, set, tuple):
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
if not args:
|
|
141
|
+
return "str_list"
|
|
142
|
+
inner = args[0]
|
|
143
|
+
# `list[tuple[str, float]]` (library import history) has its own CLI
|
|
144
|
+
# type; flattening it to a list of strings sent a shape the API refuses.
|
|
145
|
+
if typing.get_origin(inner) is tuple and list(typing.get_args(inner)) == [str, float]:
|
|
146
|
+
return "str_float_pairs"
|
|
147
|
+
# A list of objects, or of lists, has no flag spelling: take it as JSON.
|
|
148
|
+
if _is_model(inner):
|
|
149
|
+
return "json"
|
|
150
|
+
inner_type = _cli_type(inner)
|
|
151
|
+
if inner_type == "json" or inner_type.endswith(("_list", "_pairs")):
|
|
152
|
+
return "json"
|
|
153
|
+
return f"{inner_type}_list"
|
|
127
154
|
if origin is not None and args:
|
|
155
|
+
# `str | list[str]`: the list arm decides the flag's type, or a repeated
|
|
156
|
+
# flag would collapse to one string (see `is_scalar_or_list`).
|
|
157
|
+
if origin is not typing.Annotated:
|
|
158
|
+
listy = [a for a in args if typing.get_origin(a) in (list, set, tuple)]
|
|
159
|
+
if listy:
|
|
160
|
+
return _cli_type(listy[0])
|
|
128
161
|
# Optional[X] / Union[X, None] / Annotated[X, ...] -> X
|
|
129
162
|
return _cli_type(args[0])
|
|
130
163
|
for python_type, name in _SCALARS.items():
|
|
@@ -133,6 +166,29 @@ def _cli_type(annotation: Any) -> str:
|
|
|
133
166
|
return "str"
|
|
134
167
|
|
|
135
168
|
|
|
169
|
+
def _is_model(annotation: Any) -> bool:
|
|
170
|
+
from pydantic import BaseModel
|
|
171
|
+
|
|
172
|
+
return isinstance(annotation, type) and issubclass(annotation, BaseModel)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def is_scalar_or_list(annotation: Any) -> bool:
|
|
176
|
+
"""True for ``str | list[str]`` (optionally ``| None``): one value or many.
|
|
177
|
+
|
|
178
|
+
The CLI flag is repeatable. One value goes on the wire as the scalar and
|
|
179
|
+
several as a list, so ``--value my-series`` for ``move-collection`` stays a
|
|
180
|
+
string while ``--value a --value b`` for ``retag`` becomes ``["a", "b"]``.
|
|
181
|
+
"""
|
|
182
|
+
import types
|
|
183
|
+
import typing
|
|
184
|
+
|
|
185
|
+
if typing.get_origin(annotation) not in (typing.Union, types.UnionType):
|
|
186
|
+
return False
|
|
187
|
+
args = [a for a in typing.get_args(annotation) if a is not type(None)]
|
|
188
|
+
listy = [a for a in args if typing.get_origin(a) in (list, set, tuple)]
|
|
189
|
+
return bool(listy) and len(listy) < len(args)
|
|
190
|
+
|
|
191
|
+
|
|
136
192
|
#: The PATCH "leave alone" marker. A field whose annotation admits it also
|
|
137
193
|
#: admits ``None``, which means "clear" — so the CLI must be able to send null.
|
|
138
194
|
_UNSET_SENTINEL = "unset"
|
|
@@ -145,9 +201,7 @@ def _walk_annotation(annotation: Any) -> list[Any]:
|
|
|
145
201
|
"""The annotation and every type/metadata object nested inside it."""
|
|
146
202
|
import typing
|
|
147
203
|
|
|
148
|
-
seen: list[Any] = [annotation]
|
|
149
|
-
for metadata in getattr(annotation, "__metadata__", ()):
|
|
150
|
-
seen.append(metadata)
|
|
204
|
+
seen: list[Any] = [annotation, *getattr(annotation, "__metadata__", ())]
|
|
151
205
|
for arg in typing.get_args(annotation):
|
|
152
206
|
seen.extend(_walk_annotation(arg))
|
|
153
207
|
return seen
|
|
@@ -193,6 +247,15 @@ def choices_from_pattern(pattern: str | None) -> list[str] | None:
|
|
|
193
247
|
return words
|
|
194
248
|
|
|
195
249
|
|
|
250
|
+
def _max_length(field_info: Any) -> int | None:
|
|
251
|
+
"""``Field(max_length=N)`` on a list field: the most values the API accepts."""
|
|
252
|
+
for node in getattr(field_info, "metadata", None) or []:
|
|
253
|
+
value = getattr(node, "max_length", None)
|
|
254
|
+
if isinstance(value, int):
|
|
255
|
+
return value
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
|
|
196
259
|
def _pattern_of(field_info: Any) -> str | None:
|
|
197
260
|
"""The ``pattern=`` constraint on a field, wherever pydantic/FastAPI put it."""
|
|
198
261
|
nodes: list[Any] = list(getattr(field_info, "metadata", None) or [])
|
|
@@ -274,6 +337,8 @@ def _params(route: Any) -> tuple[list[dict[str, Any]], list[dict[str, Any]], lis
|
|
|
274
337
|
"required": required,
|
|
275
338
|
"multiple": cli_type.endswith("_list"),
|
|
276
339
|
}
|
|
340
|
+
if is_scalar_or_list(annotation):
|
|
341
|
+
entry["scalar_if_single"] = True
|
|
277
342
|
choices = field_choices(p.field_info)
|
|
278
343
|
if choices:
|
|
279
344
|
entry["choices"] = choices
|
|
@@ -327,6 +392,11 @@ def _body(route: Any) -> tuple[list[dict[str, Any]], bool]:
|
|
|
327
392
|
entry["required"] = True
|
|
328
393
|
if cli_type.endswith("_list"):
|
|
329
394
|
entry["multiple"] = True
|
|
395
|
+
max_items = _max_length(info)
|
|
396
|
+
if max_items is not None:
|
|
397
|
+
entry["max_items"] = max_items
|
|
398
|
+
if is_scalar_or_list(annotation):
|
|
399
|
+
entry["scalar_if_single"] = True
|
|
330
400
|
choices = field_choices(info)
|
|
331
401
|
if choices:
|
|
332
402
|
entry["choices"] = choices
|
|
@@ -334,10 +404,37 @@ def _body(route: Any) -> tuple[list[dict[str, Any]], bool]:
|
|
|
334
404
|
# `--cover-asset-id null` must reach the API as JSON null (clear),
|
|
335
405
|
# which is distinct from omitting the flag (leave alone).
|
|
336
406
|
entry["sentinel_null"] = True
|
|
407
|
+
default = info.default
|
|
408
|
+
if (
|
|
409
|
+
not info.is_required()
|
|
410
|
+
and isinstance(default, (bool, int, float, str))
|
|
411
|
+
and default != _UNSET_SENTINEL
|
|
412
|
+
):
|
|
413
|
+
# Documentation only: an unset flag is omitted and the API applies
|
|
414
|
+
# this itself. `--include-media` is worth knowing defaults to true.
|
|
415
|
+
entry["default"] = default
|
|
337
416
|
out.append(entry)
|
|
338
417
|
return out, _is_required(field)
|
|
339
418
|
|
|
340
419
|
|
|
420
|
+
def _needs_session(route: Any) -> bool:
|
|
421
|
+
"""True when the route depends on ``require_jwt_session`` at any depth.
|
|
422
|
+
|
|
423
|
+
Those routes refuse a personal API key (403) and want the bearer token of
|
|
424
|
+
an interactive login. Derived, never curated: a hand-kept list is how the
|
|
425
|
+
docs ended up telling agents to run ``lp keys permissions`` with a key.
|
|
426
|
+
"""
|
|
427
|
+
from app.core.security import require_jwt_session
|
|
428
|
+
|
|
429
|
+
stack = [route.dependant]
|
|
430
|
+
while stack:
|
|
431
|
+
dependant = stack.pop()
|
|
432
|
+
if dependant.call is require_jwt_session:
|
|
433
|
+
return True
|
|
434
|
+
stack.extend(dependant.dependencies)
|
|
435
|
+
return False
|
|
436
|
+
|
|
437
|
+
|
|
341
438
|
def _infer_auth(path: str) -> str:
|
|
342
439
|
"""Only for NEW routes; tracked ones keep whatever was curated."""
|
|
343
440
|
if path.startswith(("/admin", "/me")):
|
|
@@ -435,7 +532,7 @@ def build(legacy: bool) -> list[dict[str, Any]]:
|
|
|
435
532
|
|
|
436
533
|
entries: list[dict[str, Any]] = []
|
|
437
534
|
for full_path, route in walk_routes(app):
|
|
438
|
-
if full_path in SKIP_PATHS:
|
|
535
|
+
if full_path in SKIP_PATHS or full_path.startswith(SKIP_PREFIXES):
|
|
439
536
|
continue
|
|
440
537
|
mount = "v1" if full_path.startswith(API_V1_PREFIX) else "root"
|
|
441
538
|
path = full_path.removeprefix(API_V1_PREFIX) or "/"
|
|
@@ -457,7 +554,7 @@ def build(legacy: bool) -> list[dict[str, Any]]:
|
|
|
457
554
|
"path": path,
|
|
458
555
|
"summary": (route.summary or "").strip(),
|
|
459
556
|
"deprecated": bool(route.deprecated),
|
|
460
|
-
"auth": _infer_auth(path),
|
|
557
|
+
"auth": "session" if _needs_session(route) else _infer_auth(path),
|
|
461
558
|
"path_params": path_params,
|
|
462
559
|
"query": query,
|
|
463
560
|
"headers": headers,
|
|
@@ -516,6 +613,12 @@ def merge(
|
|
|
516
613
|
kept = dict(entry)
|
|
517
614
|
for field in ("id", "group", "name", "auth", "stream"):
|
|
518
615
|
kept[field] = existing[field]
|
|
616
|
+
# "session" is derived from the route's dependencies, so it overrides a
|
|
617
|
+
# curated value in both directions: gained or lost with the dependency.
|
|
618
|
+
if entry["auth"] == "session":
|
|
619
|
+
kept["auth"] = "session"
|
|
620
|
+
elif existing["auth"] == "session":
|
|
621
|
+
kept["auth"] = entry["auth"]
|
|
519
622
|
# A curated body is richer than the derived one only when derivation
|
|
520
623
|
# found nothing at all.
|
|
521
624
|
if existing.get("body") and not kept["body"]:
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# lp-cli — an agent skill for the Lekha Poth CLI
|
|
2
|
+
|
|
3
|
+
This directory is a self-contained [agent skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills)
|
|
4
|
+
that teaches an LLM agent to operate [Lekha Poth](https://www.lekhapoth.com) through
|
|
5
|
+
`lp`, the full-coverage command-line client published on PyPI as
|
|
6
|
+
[`lekha-poth-cli`](https://pypi.org/project/lekha-poth-cli/).
|
|
7
|
+
|
|
8
|
+
You do **not** need the Lekha Poth source repository to use it. You need:
|
|
9
|
+
|
|
10
|
+
1. **`uv`** — `curl -LsSf https://astral.sh/uv/install.sh | sh` (macOS/Linux).
|
|
11
|
+
2. **The CLI** — `uv tool install lekha-poth-cli`, which puts `lp` at
|
|
12
|
+
`$HOME/.local/bin/lp`. Keep it current with `uv tool upgrade lekha-poth-cli`.
|
|
13
|
+
3. **This directory**, placed where your agent loads skills from.
|
|
14
|
+
|
|
15
|
+
## Installing the skill
|
|
16
|
+
|
|
17
|
+
Copy the whole `lp-cli/` directory (keep `SKILL.md`, `references/`, `scripts/`
|
|
18
|
+
together) into your agent's skills location:
|
|
19
|
+
|
|
20
|
+
| Agent | Where |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Claude Code, for one user | `~/.claude/skills/lp-cli/` |
|
|
23
|
+
| Claude Code, for one project | `<project>/.claude/skills/lp-cli/` |
|
|
24
|
+
| Any other agent that reads `SKILL.md` frontmatter | its skills directory |
|
|
25
|
+
|
|
26
|
+
Or package it as a single file with the skill-creator's
|
|
27
|
+
`package_skill.py` and install the resulting `lp-cli.skill`.
|
|
28
|
+
|
|
29
|
+
The agent reads `SKILL.md` first. It holds the install and pre-flight steps,
|
|
30
|
+
task recipes, the exit-code table and the wire conventions, and routes to one
|
|
31
|
+
of the `references/*.md` files for a command's exact flags.
|
|
32
|
+
|
|
33
|
+
## Layout
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
lp-cli/
|
|
37
|
+
├── SKILL.md # entry point: pre-flight, recipes, conventions, errors
|
|
38
|
+
├── README.md # this file (for humans)
|
|
39
|
+
├── references/
|
|
40
|
+
│ ├── public-content.md # generated — items, collections, search, tags, sitemap, stats
|
|
41
|
+
│ ├── auth-account.md # generated — auth, /me, API keys
|
|
42
|
+
│ ├── library.md # generated — reader library + reports
|
|
43
|
+
│ ├── chats.md # generated — persona chat
|
|
44
|
+
│ ├── admin.md # generated — every /admin/* command
|
|
45
|
+
│ ├── legacy.md # generated — deprecated /chapters, /series aliases
|
|
46
|
+
│ ├── workflows.md # hand-written — setup, login, media, publish (the non-generated commands)
|
|
47
|
+
│ ├── manifest.md # hand-written — `lp publish manifest` JSON schema
|
|
48
|
+
│ └── vocab.md # hand-written — what each allowed value means
|
|
49
|
+
├── scripts/
|
|
50
|
+
│ └── generate_references.py # rebuilds references/*.md from the CLI's endpoints.json
|
|
51
|
+
└── evals/
|
|
52
|
+
└── evals.json # test prompts used to evaluate the skill
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Keeping the references in step with your CLI
|
|
56
|
+
|
|
57
|
+
The six generated reference files describe the CLI at contract **2.3.0**. If
|
|
58
|
+
`lp --version` reports something newer, rebuild them from the spec that ships
|
|
59
|
+
inside the installed package — no checkout required:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv run --no-project --with lekha-poth-cli python3 scripts/generate_references.py # rewrite
|
|
63
|
+
uv run --no-project --with lekha-poth-cli python3 scripts/generate_references.py --check # report only
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`SKILL.md`, `references/vocab.md`, `references/workflows.md` and
|
|
67
|
+
`references/manifest.md` are hand-written and are not touched by the script;
|
|
68
|
+
where they and `lp <command> --help` disagree, `--help` is current.
|
|
69
|
+
|
|
70
|
+
## For maintainers (inside the Lekha Poth repository)
|
|
71
|
+
|
|
72
|
+
The skill lives at `scripts/agent/lpcli/skills/lp-cli/` and CI enforces that
|
|
73
|
+
it matches the API. After any backend route or parameter change, from the
|
|
74
|
+
repository root:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cd backend && uv run python ../scripts/agent/lpcli/scripts/generate_spec.py # endpoints.json (types, choices, sentinels)
|
|
78
|
+
cd .. && python3 scripts/agent/lpcli/scripts/generate_catalog_v2.py # docs/agent/API_CATALOG_V2.md
|
|
79
|
+
python3 scripts/agent/lpcli/skills/lp-cli/scripts/generate_references.py # references/*.md
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`backend/tests/unit/test_agent_spec_current.py` compares the spec with the
|
|
83
|
+
live app; `scripts/agent/lpcli/tests/test_references_current.py` and
|
|
84
|
+
`test_catalog_v2.py` check the two derived documents. When the skill is run
|
|
85
|
+
from inside the checkout, `generate_references.py` reads the repository's
|
|
86
|
+
`endpoints.json` rather than the installed package's, so the check is against
|
|
87
|
+
the code under review. Allowed values are derived from the backend's
|
|
88
|
+
`Literal` and `pattern=` validators, so a new vocabulary appears on its own.
|
|
89
|
+
What still needs a human:
|
|
90
|
+
|
|
91
|
+
- **Behaviour that isn't visible from the schema.** Record it in
|
|
92
|
+
`ENDPOINT_NOTES` / `FIELD_VOCAB_HINTS` in `generate_references.py`, only
|
|
93
|
+
after reading the backend code.
|
|
94
|
+
- **`SKILL.md`, `references/vocab.md`, `workflows.md`, `manifest.md`.** Every
|
|
95
|
+
claim should name the source file it was checked against. A change to a
|
|
96
|
+
hand-written command (`app.py`, `auth_flow.py`, `media.py`, `publish.py`)
|
|
97
|
+
means updating `workflows.md` / `manifest.md`.
|
|
98
|
+
- **A new route** needs an entry in `NEW_IDENTITY` in `generate_spec.py`; the
|
|
99
|
+
generator refuses to invent command names.
|
|
100
|
+
- **A CLI behaviour change** ships through **Actions → Release
|
|
101
|
+
(lekha-poth-cli)**, which runs `scripts/ci/stamp-lpcli-version.py` to update
|
|
102
|
+
every copy of the contract version (package, this skill's title, §0 and §11,
|
|
103
|
+
this README, `agent/SKILL.md`, `public/.well-known/agent.json`) and
|
|
104
|
+
publishes to PyPI. Don't bump them by hand: a user who only has
|
|
105
|
+
`uv tool upgrade` sees nothing until the release.
|