lekha-poth-cli 2.2.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.2.0 → lekha_poth_cli-2.3.0}/PKG-INFO +5 -5
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/README.md +4 -4
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/pyproject.toml +1 -1
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/scripts/generate_spec.py +98 -4
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/README.md +17 -10
- 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.2.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/admin.md +24 -20
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/auth-account.md +56 -20
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/chats.md +1 -1
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/legacy.md +7 -7
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/library.md +9 -9
- lekha_poth_cli-2.3.0/skills/lp-cli/references/manifest.md +89 -0
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/public-content.md +3 -1
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/references/vocab.md +39 -22
- lekha_poth_cli-2.3.0/skills/lp-cli/references/workflows.md +183 -0
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/skills/lp-cli/scripts/generate_references.py +66 -15
- {lekha_poth_cli-2.2.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.2.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/config.py +62 -4
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/endpoints.json +76 -49
- {lekha_poth_cli-2.2.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.2.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/register.py +50 -11
- {lekha_poth_cli-2.2.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.2.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.2.0 → lekha_poth_cli-2.3.0}/tests/test_spec_fields.py +161 -0
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/uv.lock +117 -119
- lekha_poth_cli-2.2.0/skills/lp-cli/SKILL.md +0 -383
- lekha_poth_cli-2.2.0/skills/lp-cli/evals/evals.json +0 -108
- lekha_poth_cli-2.2.0/src/lekha_poth_cli/app.py +0 -287
- lekha_poth_cli-2.2.0/src/lekha_poth_cli/client.py +0 -149
- lekha_poth_cli-2.2.0/src/lekha_poth_cli/media.py +0 -94
- lekha_poth_cli-2.2.0/tests/test_config.py +0 -28
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/.gitignore +0 -0
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/scripts/generate_catalog_v2.py +0 -0
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/__main__.py +0 -0
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/src/lekha_poth_cli/output.py +0 -0
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/tests/test_catalog_v2.py +0 -0
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/tests/test_contract_version_parity.py +0 -0
- {lekha_poth_cli-2.2.0 → lekha_poth_cli-2.3.0}/tests/test_coverage.py +0 -0
- {lekha_poth_cli-2.2.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,12 +17,12 @@ 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
|
|
|
@@ -68,7 +68,7 @@ lp config set --api-base "$LEKHA_POTH_API_BASE" \
|
|
|
68
68
|
--site-url "$LEKHA_POTH_SITE_URL" \
|
|
69
69
|
--api-key lpak_…
|
|
70
70
|
# hosted production example:
|
|
71
|
-
# --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
|
|
72
72
|
lp config set-defaults --output pretty --default-size 20
|
|
73
73
|
lp status
|
|
74
74
|
lp whoami
|
|
@@ -1,12 +1,12 @@
|
|
|
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
|
|
|
@@ -52,7 +52,7 @@ lp config set --api-base "$LEKHA_POTH_API_BASE" \
|
|
|
52
52
|
--site-url "$LEKHA_POTH_SITE_URL" \
|
|
53
53
|
--api-key lpak_…
|
|
54
54
|
# hosted production example:
|
|
55
|
-
# --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
|
|
56
56
|
lp config set-defaults --output pretty --default-size 20
|
|
57
57
|
lp status
|
|
58
58
|
lp whoami
|
|
@@ -64,6 +64,10 @@ API_V1_PREFIX = "/api/v1"
|
|
|
64
64
|
#: Routes that are infrastructure, not API surface.
|
|
65
65
|
SKIP_PATHS = {"/docs", "/openapi.json", "/redoc", "/metrics"}
|
|
66
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
|
+
|
|
67
71
|
|
|
68
72
|
def _ensure_backend_importable() -> None:
|
|
69
73
|
src = REPO_ROOT / "backend" / "src"
|
|
@@ -133,9 +137,27 @@ def _cli_type(annotation: Any) -> str:
|
|
|
133
137
|
if annotation in (dict, Any) or origin is dict:
|
|
134
138
|
return "json"
|
|
135
139
|
if origin in (list, set, tuple):
|
|
136
|
-
|
|
137
|
-
|
|
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"
|
|
138
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])
|
|
139
161
|
# Optional[X] / Union[X, None] / Annotated[X, ...] -> X
|
|
140
162
|
return _cli_type(args[0])
|
|
141
163
|
for python_type, name in _SCALARS.items():
|
|
@@ -144,6 +166,29 @@ def _cli_type(annotation: Any) -> str:
|
|
|
144
166
|
return "str"
|
|
145
167
|
|
|
146
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
|
+
|
|
147
192
|
#: The PATCH "leave alone" marker. A field whose annotation admits it also
|
|
148
193
|
#: admits ``None``, which means "clear" — so the CLI must be able to send null.
|
|
149
194
|
_UNSET_SENTINEL = "unset"
|
|
@@ -202,6 +247,15 @@ def choices_from_pattern(pattern: str | None) -> list[str] | None:
|
|
|
202
247
|
return words
|
|
203
248
|
|
|
204
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
|
+
|
|
205
259
|
def _pattern_of(field_info: Any) -> str | None:
|
|
206
260
|
"""The ``pattern=`` constraint on a field, wherever pydantic/FastAPI put it."""
|
|
207
261
|
nodes: list[Any] = list(getattr(field_info, "metadata", None) or [])
|
|
@@ -283,6 +337,8 @@ def _params(route: Any) -> tuple[list[dict[str, Any]], list[dict[str, Any]], lis
|
|
|
283
337
|
"required": required,
|
|
284
338
|
"multiple": cli_type.endswith("_list"),
|
|
285
339
|
}
|
|
340
|
+
if is_scalar_or_list(annotation):
|
|
341
|
+
entry["scalar_if_single"] = True
|
|
286
342
|
choices = field_choices(p.field_info)
|
|
287
343
|
if choices:
|
|
288
344
|
entry["choices"] = choices
|
|
@@ -336,6 +392,11 @@ def _body(route: Any) -> tuple[list[dict[str, Any]], bool]:
|
|
|
336
392
|
entry["required"] = True
|
|
337
393
|
if cli_type.endswith("_list"):
|
|
338
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
|
|
339
400
|
choices = field_choices(info)
|
|
340
401
|
if choices:
|
|
341
402
|
entry["choices"] = choices
|
|
@@ -343,10 +404,37 @@ def _body(route: Any) -> tuple[list[dict[str, Any]], bool]:
|
|
|
343
404
|
# `--cover-asset-id null` must reach the API as JSON null (clear),
|
|
344
405
|
# which is distinct from omitting the flag (leave alone).
|
|
345
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
|
|
346
416
|
out.append(entry)
|
|
347
417
|
return out, _is_required(field)
|
|
348
418
|
|
|
349
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
|
+
|
|
350
438
|
def _infer_auth(path: str) -> str:
|
|
351
439
|
"""Only for NEW routes; tracked ones keep whatever was curated."""
|
|
352
440
|
if path.startswith(("/admin", "/me")):
|
|
@@ -444,7 +532,7 @@ def build(legacy: bool) -> list[dict[str, Any]]:
|
|
|
444
532
|
|
|
445
533
|
entries: list[dict[str, Any]] = []
|
|
446
534
|
for full_path, route in walk_routes(app):
|
|
447
|
-
if full_path in SKIP_PATHS:
|
|
535
|
+
if full_path in SKIP_PATHS or full_path.startswith(SKIP_PREFIXES):
|
|
448
536
|
continue
|
|
449
537
|
mount = "v1" if full_path.startswith(API_V1_PREFIX) else "root"
|
|
450
538
|
path = full_path.removeprefix(API_V1_PREFIX) or "/"
|
|
@@ -466,7 +554,7 @@ def build(legacy: bool) -> list[dict[str, Any]]:
|
|
|
466
554
|
"path": path,
|
|
467
555
|
"summary": (route.summary or "").strip(),
|
|
468
556
|
"deprecated": bool(route.deprecated),
|
|
469
|
-
"auth": _infer_auth(path),
|
|
557
|
+
"auth": "session" if _needs_session(route) else _infer_auth(path),
|
|
470
558
|
"path_params": path_params,
|
|
471
559
|
"query": query,
|
|
472
560
|
"headers": headers,
|
|
@@ -525,6 +613,12 @@ def merge(
|
|
|
525
613
|
kept = dict(entry)
|
|
526
614
|
for field in ("id", "group", "name", "auth", "stream"):
|
|
527
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"]
|
|
528
622
|
# A curated body is richer than the derived one only when derivation
|
|
529
623
|
# found nothing at all.
|
|
530
624
|
if existing.get("body") and not kept["body"]:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# lp-cli — an agent skill for the Lekha Poth CLI
|
|
2
2
|
|
|
3
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://lekhapoth.com) through
|
|
4
|
+
that teaches an LLM agent to operate [Lekha Poth](https://www.lekhapoth.com) through
|
|
5
5
|
`lp`, the full-coverage command-line client published on PyPI as
|
|
6
6
|
[`lekha-poth-cli`](https://pypi.org/project/lekha-poth-cli/).
|
|
7
7
|
|
|
@@ -43,6 +43,8 @@ lp-cli/
|
|
|
43
43
|
│ ├── chats.md # generated — persona chat
|
|
44
44
|
│ ├── admin.md # generated — every /admin/* command
|
|
45
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
|
|
46
48
|
│ └── vocab.md # hand-written — what each allowed value means
|
|
47
49
|
├── scripts/
|
|
48
50
|
│ └── generate_references.py # rebuilds references/*.md from the CLI's endpoints.json
|
|
@@ -52,7 +54,7 @@ lp-cli/
|
|
|
52
54
|
|
|
53
55
|
## Keeping the references in step with your CLI
|
|
54
56
|
|
|
55
|
-
The six generated reference files describe the CLI at contract **2.
|
|
57
|
+
The six generated reference files describe the CLI at contract **2.3.0**. If
|
|
56
58
|
`lp --version` reports something newer, rebuild them from the spec that ships
|
|
57
59
|
inside the installed package — no checkout required:
|
|
58
60
|
|
|
@@ -61,8 +63,9 @@ uv run --no-project --with lekha-poth-cli python3 scripts/generate_references.py
|
|
|
61
63
|
uv run --no-project --with lekha-poth-cli python3 scripts/generate_references.py --check # report only
|
|
62
64
|
```
|
|
63
65
|
|
|
64
|
-
`SKILL.md
|
|
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.
|
|
66
69
|
|
|
67
70
|
## For maintainers (inside the Lekha Poth repository)
|
|
68
71
|
|
|
@@ -88,11 +91,15 @@ What still needs a human:
|
|
|
88
91
|
- **Behaviour that isn't visible from the schema.** Record it in
|
|
89
92
|
`ENDPOINT_NOTES` / `FIELD_VOCAB_HINTS` in `generate_references.py`, only
|
|
90
93
|
after reading the backend code.
|
|
91
|
-
- **`references/vocab.md`
|
|
92
|
-
source file it was checked against.
|
|
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`.
|
|
93
98
|
- **A new route** needs an entry in `NEW_IDENTITY` in `generate_spec.py`; the
|
|
94
99
|
generator refuses to invent command names.
|
|
95
|
-
- **A CLI behaviour change**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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.
|