feat(config): triage agent, rotation and vibegram jobs, deploy and komodo hooks, vault watch rules

This commit is contained in:
hh
2026-08-29 02:29:08 +02:00
parent c830ca73e6
commit 80806d18d8
3 changed files with 304 additions and 5 deletions
+25
View File
@@ -104,6 +104,31 @@ docker compose up -d
Set `PUBLIC_BASE_URL=https://<DOMAIN>` in beaver-agent's `.env` so advertised endpoints use the outside origin. Set `PUBLIC_BASE_URL=https://<DOMAIN>` in beaver-agent's `.env` so advertised endpoints use the outside origin.
## Deploy: `main` is work, `stable` is production
Komodo deploys only the `stable` ref; pushing `main` deploys nothing. Commit
during the day, then ship when the master thread is idle:
```bash
make deploy # git push origin main:stable
```
The gateway image is built from `beaver-gateway#${GATEWAY_REF}` at deploy
time, so push the gateway first. `docker-compose.yml` has a healthcheck on
`/healthz`; Komodo waits for it before it considers the deploy done.
To drain first (архитектура §8.6), call the deploy hook instead of pushing
by hand: it waits until no master turn is running, then asks Komodo:
```bash
curl -X POST https://<DOMAIN>/hooks/deploy -H "Authorization: Bearer <TOKEN>"
```
Needs `KOMODO_URL` / `KOMODO_KEY` / `KOMODO_SECRET` in `.env` (and
`KOMODO_STACK`, default `beaver-agent`). Komodo alerts can be pointed at
`POST /hooks/komodo` the same way - they land in the master as an urgent
inject. Jobs, the queue and the subscription window are on the admin **Jobs** page.
## Editing config.py on the mac ## Editing config.py on the mac
`make sync` installs `../beaver-gateway` editable (extra `local`), so `make sync` installs `../beaver-gateway` editable (extra `local`), so
+189 -5
View File
@@ -1,14 +1,20 @@
import logging
import os import os
from datetime import date, timedelta from datetime import UTC, date, datetime, timedelta
from pathlib import Path from pathlib import Path
from zoneinfo import ZoneInfo
import aiohttp
from beaver_gateway.agents.base import BaseAgent, ExposedMcp from beaver_gateway.agents.base import BaseAgent, ExposedMcp
from beaver_gateway.agents.claude import ClaudeAgent, ClaudeOptions, Prompts from beaver_gateway.agents.claude import ClaudeAgent, ClaudeOptions, Prompts
from beaver_gateway.agents.raycast import RaycastAgent, RemoteTool, UserPreferences from beaver_gateway.agents.raycast import RaycastAgent, RemoteTool, UserPreferences
from beaver_gateway.core.conversations import ConversationTexts, SeedContext from beaver_gateway.core.conversations import ConversationTexts, SeedContext
from beaver_gateway.core.prompt import assemble from beaver_gateway.core.prompt import assemble
from beaver_gateway.core.registry import Gateway from beaver_gateway.core.registry import Gateway
from beaver_gateway.core.rotation import HandoutContext, RotationPolicy
from beaver_gateway.core.scheduler import Budget, Job, JobRun
from beaver_gateway.core.turn_record import slugify from beaver_gateway.core.turn_record import slugify
from beaver_gateway.core.watch import VaultWatch, WatchRules
from beaver_gateway.frontends.admin import AdminFrontend from beaver_gateway.frontends.admin import AdminFrontend
from beaver_gateway.frontends.anthropic import AnthropicMessagesFrontend from beaver_gateway.frontends.anthropic import AnthropicMessagesFrontend
from beaver_gateway.frontends.api import ApiFrontend from beaver_gateway.frontends.api import ApiFrontend
@@ -18,12 +24,17 @@ from beaver_gateway.frontends.mcp_server import McpServerFrontend
from beaver_gateway.frontends.telegram import TelegramFrontend from beaver_gateway.frontends.telegram import TelegramFrontend
from beaver_gateway.mcp.types import HttpMcp, McpServer, McpServerT from beaver_gateway.mcp.types import HttpMcp, McpServer, McpServerT
TZ = "Europe/Warsaw"
VAULT = Path("/vault") VAULT = Path("/vault")
CHATS_DIR = VAULT / "💬 чаты" CHATS_DIR = VAULT / "💬 чаты"
DIARY = VAULT / "📅 дни"
BEAVER = VAULT / "мета" / "бобер" BEAVER = VAULT / "мета" / "бобер"
PROMPTS = BEAVER / "промпты" PROMPTS = BEAVER / "промпты"
GRANULES = PROMPTS / "гранулы" GRANULES = PROMPTS / "гранулы"
SKILLS = BEAVER / "скиллы" SKILLS = BEAVER / "скиллы"
DAYS = BEAVER / "дни"
_log = logging.getLogger("beaver_agent.config")
# Гранулы - чистый markdown; xml-теги, в которые они заворачиваются, # Гранулы - чистый markdown; xml-теги, в которые они заворачиваются,
# живут здесь. Голос - папка, файл на тег, в этом порядке. # живут здесь. Голос - папка, файл на тег, в этом порядке.
@@ -68,6 +79,8 @@ DISPATCHER_PROMPTS = Prompts(
fork=_DISTILL, fork=_DISTILL,
job=_DISTILL, job=_DISTILL,
) )
# §4.1: триаж без голоса и без vault - профиль, окружение джоба, оверлей.
TRIAGE_PROMPTS = Prompts(job=(PROFILE, environment("джоб"), overlay("triage", "триаж")))
QUICK_PROMPT = assemble((*VOICE, PROFILE, overlay("quick", "быстрый"))) QUICK_PROMPT = assemble((*VOICE, PROFILE, overlay("quick", "быстрый")))
# §4.3: наборы скиллов = папки = плагины. Глубокие грузят общие + vault. # §4.3: наборы скиллов = папки = плагины. Глубокие грузят общие + vault.
@@ -175,6 +188,22 @@ def deep(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
) )
def triage(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
"""§4.1: дешёвый крон-тёрн - sonnet low, без vault, только say/inject."""
return ClaudeAgent(
name=name,
model=model,
cwd=Path("/tmp"), # noqa: S108 - не vault: у триажа нет файлов вообще
prompts=TRIAGE_PROMPTS,
gateway_tools=("say", "inject"),
options=ClaudeOptions(
effort=effort,
tools=(),
disallowed_tools=("AskUserQuestion", "Task", "WebSearch", "WebFetch"),
),
)
def raycast(name: str, model: str, reasoning_effort: str | None = None) -> RaycastAgent: def raycast(name: str, model: str, reasoning_effort: str | None = None) -> RaycastAgent:
return RaycastAgent( return RaycastAgent(
name=name, name=name,
@@ -201,6 +230,7 @@ agents: list[BaseAgent] = [
deep("beaver-opus-medium", "claude-opus-5", effort="medium"), deep("beaver-opus-medium", "claude-opus-5", effort="medium"),
deep("beaver-fable-medium", "claude-fable-5", effort="medium"), deep("beaver-fable-medium", "claude-fable-5", effort="medium"),
deep("beaver-opus-xhigh", "claude-opus-5", effort="xhigh"), deep("beaver-opus-xhigh", "claude-opus-5", effort="xhigh"),
triage("beaver-triage", "claude-sonnet-5", effort="low"),
raycast("beaver-gemini-pro-low", "google-gemini-3.1-pro", reasoning_effort="low"), raycast("beaver-gemini-pro-low", "google-gemini-3.1-pro", reasoning_effort="low"),
raycast( raycast(
"beaver-gemini-flash-high", "google-gemini-3.5-flash", reasoning_effort="high" "beaver-gemini-flash-high", "google-gemini-3.5-flash", reasoning_effort="high"
@@ -252,25 +282,179 @@ def _read(path: Path) -> str | None:
return path.read_text(encoding="utf-8").strip() if path.exists() else None return path.read_text(encoding="utf-8").strip() if path.exists() else None
def _today() -> date:
return datetime.now(UTC).astimezone(ZoneInfo(TZ)).date()
def latest_handout() -> Path | None:
"""§4.5: сид morning берёт последний хендаут, не «вчерашний по календарю»."""
files = sorted(DAYS.glob("????-??-??.md")) if DAYS.exists() else []
return files[-1] if files else None
def seed_body(ctx: SeedContext) -> str | None: def seed_body(ctx: SeedContext) -> str | None:
"""§8.2: утренний сид - вчерашний хендаут из `дни/`; остальные сиды - gateway.""" """§8.2: утренний сид - последний хендаут из `дни/`; остальные сиды - gateway."""
if ctx.seed != "morning": if ctx.seed != "morning":
return None return None
yesterday = date.today() - timedelta(days=1) handout = latest_handout()
handout = _read(BEAVER / "дни" / f"{yesterday:%Y-%m-%d}.md") if handout is None:
return handout or "Хендаут за вчера не приехал." return "Хендаут не приехал."
state = _read(BEAVER / "состояние.md") or ""
signal = "\n".join(state.splitlines()[:5])
parts = [
f"Хендаут ({handout.stem}):\n\n{handout.read_text(encoding='utf-8').strip()}"
]
if signal:
parts.append(
f"Состояние (первые строки, полностью в мета/бобер/состояние.md):\n{signal}"
)
return "\n\n".join(parts)
DEFAULT_HANDOUT = (
"Этот мастер закрывается ({reason}). Напиши хендаут за {day} в "
"`мета/бобер/дни/{day}.md`: справку на утро, не задание."
)
def handout_prompt(ctx: HandoutContext) -> str:
"""§6.3, §8.3: последний тёрн закрываемого мастера - гранула + факт про дневник."""
day = ctx.day.isoformat()
body = (
(_read(GRANULES / "хендаут.md") or DEFAULT_HANDOUT)
.replace("{day}", day)
.replace("{reason}", ctx.reason)
)
diary = DIARY / f"{day}.md"
if diary.exists():
return f"{body}\n\nДневник за {day} приехал: `📅 дни/{day}.md`, возьми из него."
return f"{body}\n\nДневник за {day} не приехал - так и напиши в разделе «дневник»."
NEW_DAY = (
"Новый день ({day}). Ты - новый мастер того же треда: хендаут и сигнал состояния "
"в сиде выше. Скажи Бобру что-то через say, только если есть что сказать."
)
texts = ConversationTexts( texts = ConversationTexts(
merge_prompt=_read(GRANULES / "слив.md") or ConversationTexts().merge_prompt, merge_prompt=_read(GRANULES / "слив.md") or ConversationTexts().merge_prompt,
seed=seed_body, seed=seed_body,
handout=handout_prompt,
new_day=NEW_DAY,
) )
# §4.6: конверт - полный дифф только у дневника за сегодня, остальное именами;
# самый конкретный паттерн побеждает (мета/бобер видно, остальная мета - нет).
watch = VaultWatch(
VAULT,
WatchRules(
full=("📅 дни/{today}.md",),
names=(
"📅 дни/**",
"👤 люди/**",
"📆 планирование/**",
"📆 доски/**",
"💻 проекты/**",
"🧠 мысли/**",
"📶 ресерчи/**",
"мета/бобер/**",
),
ignore=("💬 чаты/**", "мета/**", "**/attachments/**", ".obsidian/**"),
),
tz=TZ,
)
# §4.5 джобы. Хендлер только ставит работу в очередь и выходит.
async def rotate(run: JobRun) -> None:
for master in await run.rotate():
_log.info("rotated master -> %s", master.external_id)
_vibegram_last_wake: datetime | None = None
def vibegram_check() -> list[str]:
"""Заглушка до M7: детерминированная проверка вайбграма, пока ничего нового."""
return []
async def vibegram(run: JobRun) -> None:
global _vibegram_last_wake # noqa: PLW0603 - счётчик «≤ 1/ч», живёт до рестарта
new = vibegram_check()
if not new:
return
now = datetime.now(UTC)
if _vibegram_last_wake is not None and now - _vibegram_last_wake < timedelta(
hours=1
):
return
_vibegram_last_wake = now
await run.spawn_job(
agent="beaver-triage",
title="вайбграм",
text="Новое в вайбграме:\n" + "\n".join(f"- {item}" for item in new),
)
KOMODO_STACK = os.environ.get("KOMODO_STACK", "beaver-agent")
async def komodo_deploy() -> None:
url, key, secret = (
os.environ.get(k, "") for k in ("KOMODO_URL", "KOMODO_KEY", "KOMODO_SECRET")
)
if not (url and key and secret):
_log.warning("deploy hook: KOMODO_URL/KEY/SECRET are not set, nothing deployed")
return
body = {"type": "DeployStack", "params": {"stack": KOMODO_STACK, "services": []}}
headers = {"X-Api-Key": key, "X-Api-Secret": secret}
async with (
aiohttp.ClientSession() as http,
http.post(
f"{url.rstrip('/')}/execute",
json=body,
headers=headers,
timeout=aiohttp.ClientTimeout(total=30),
) as response,
):
_log.info(
"deploy hook: komodo %s %s", response.status, (await response.text())[:200]
)
async def deploy(run: JobRun) -> None:
"""§8.6: деплой ждёт conversation.idle(master) - иначе рестарт посреди тёрна."""
master = await run.master()
if master is not None and await run.conversations.busy(master):
await run.retry_in(timedelta(seconds=60))
return
run.background(komodo_deploy())
async def komodo_alert(run: JobRun) -> None:
payload = run.payload
summary = payload.get("raw") or {k: v for k, v in payload.items() if k != "trigger"}
await run.inject_master(f"Komodo: {summary}", urgency="urgent", origin="komodo")
jobs = [
Job("ротация", rotate, cron="0 * * * *"),
Job("вайбграм", vibegram, cron="*/10 * * * *", critical=False),
Job("deploy", deploy, webhook=True),
Job("komodo", komodo_alert, webhook=True),
]
gateway = Gateway( gateway = Gateway(
agents=agents, agents=agents,
mcps=mcps, mcps=mcps,
frontends=frontends, frontends=frontends,
texts=texts, texts=texts,
jobs=jobs,
rotation=RotationPolicy(tz=TZ),
watch=watch,
budget=Budget(threshold=0.7),
tz=TZ,
port=62990, port=62990,
public_url=PUBLIC_BASE_URL or None, public_url=PUBLIC_BASE_URL or None,
) )
Generated
+90
View File
@@ -360,10 +360,12 @@ dependencies = [
{ name = "anyio" }, { name = "anyio" },
{ name = "argon2-cffi" }, { name = "argon2-cffi" },
{ name = "claude-agent-sdk" }, { name = "claude-agent-sdk" },
{ name = "croniter" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "fastmcp" }, { name = "fastmcp" },
{ name = "greenlet" }, { name = "greenlet" },
{ name = "itsdangerous" }, { name = "itsdangerous" },
{ name = "pgqueuer" },
{ name = "psutil" }, { name = "psutil" },
{ name = "psycopg", extra = ["binary"], marker = "extra == 'extra-12-beaver-agent-local'" }, { name = "psycopg", extra = ["binary"], marker = "extra == 'extra-12-beaver-agent-local'" },
{ name = "pydantic" }, { name = "pydantic" },
@@ -372,6 +374,7 @@ dependencies = [
{ name = "sqlmodel" }, { name = "sqlmodel" },
{ name = "uvicorn", extra = ["standard"], marker = "extra == 'extra-12-beaver-agent-local'" }, { name = "uvicorn", extra = ["standard"], marker = "extra == 'extra-12-beaver-agent-local'" },
{ name = "uvloop" }, { name = "uvloop" },
{ name = "watchfiles" },
] ]
[package.metadata] [package.metadata]
@@ -384,10 +387,12 @@ requires-dist = [
{ name = "anyio", specifier = ">=4.13.0" }, { name = "anyio", specifier = ">=4.13.0" },
{ name = "argon2-cffi", specifier = ">=25.1.0" }, { name = "argon2-cffi", specifier = ">=25.1.0" },
{ name = "claude-agent-sdk", specifier = ">=0.2.146" }, { name = "claude-agent-sdk", specifier = ">=0.2.146" },
{ name = "croniter", specifier = ">=6.2.4" },
{ name = "fastapi", specifier = ">=0.136.1" }, { name = "fastapi", specifier = ">=0.136.1" },
{ name = "fastmcp", specifier = ">=3.3.1" }, { name = "fastmcp", specifier = ">=3.3.1" },
{ name = "greenlet", specifier = ">=3.5.0" }, { name = "greenlet", specifier = ">=3.5.0" },
{ name = "itsdangerous", specifier = ">=2.2.0" }, { name = "itsdangerous", specifier = ">=2.2.0" },
{ name = "pgqueuer", specifier = ">=1.3.2" },
{ name = "psutil", specifier = ">=7.2.2" }, { name = "psutil", specifier = ">=7.2.2" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.3.4" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.3.4" },
{ name = "pydantic", specifier = ">=2.13.4" }, { name = "pydantic", specifier = ">=2.13.4" },
@@ -398,6 +403,7 @@ requires-dist = [
{ name = "sqlmodel", specifier = ">=0.0.38" }, { name = "sqlmodel", specifier = ">=0.0.38" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.47.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.47.0" },
{ name = "uvloop", specifier = ">=0.22.1" }, { name = "uvloop", specifier = ">=0.22.1" },
{ name = "watchfiles", specifier = ">=1.2.0" },
] ]
provides-extras = ["local", "prod"] provides-extras = ["local", "prod"]
@@ -538,6 +544,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "croniter"
version = "6.2.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/37/57/2e2a65aee2a70483cb28e2b7e15a072d00a523207593b44400d4717bb100/croniter-6.2.4.tar.gz", hash = "sha256:fc124f751b1b04805c2a04b061898b436b45ab2320b045e1e052ea895de65189", size = 166267, upload-time = "2026-07-10T09:52:59.955Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/ba/d678e5bd329646ca51d3c92addbc77804e86d21f4b6b6a027218e6abb010/croniter-6.2.4-py3-none-any.whl", hash = "sha256:8ef3d544107a5c05a150a2d78f8bf5a8eb9c5c4d93405a736b824109574e3f4d", size = 46677, upload-time = "2026-07-10T09:52:58.425Z" },
]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "48.0.0" version = "48.0.0"
@@ -1389,6 +1407,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" }, { url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" },
] ]
[[package]]
name = "pgqueuer"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "croniter" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "tabulate" },
{ name = "typer" },
{ name = "uvloop", marker = "sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/33/10/6af18eede8a31c6d016564fd815d0cf1aaa69b8e584108a604b88f1ea56c/pgqueuer-1.3.2.tar.gz", hash = "sha256:11f4c67d9dfc343c02d8bb2e362733a0d44272f6c7e846630fe198f688fa3ed1", size = 505605, upload-time = "2026-07-27T16:58:00.864Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/16/1e501ee36fb8e6bdde6a3a4dd9b10d1b45c548e02f9c1f11e9ffc62ee922/pgqueuer-1.3.2-py3-none-any.whl", hash = "sha256:0823fcb089bb3ffd0580518e91b53888c9be742b42e706f9860b6a533f32bc44", size = 146747, upload-time = "2026-07-27T16:57:59.153Z" },
]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.9.6" version = "4.9.6"
@@ -1714,6 +1750,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
] ]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.2.2" version = "1.2.2"
@@ -1958,6 +2006,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
] ]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.1" version = "1.3.1"
@@ -2045,6 +2111,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
] ]
[[package]]
name = "tabulate"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" },
]
[[package]] [[package]]
name = "ty" name = "ty"
version = "0.0.75" version = "0.0.75"
@@ -2070,6 +2145,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/73/4542f829107468b5de4231af67f29927c093bfad11f3c1e5b2c08fb1206b/ty-0.0.75-py3-none-win_arm64.whl", hash = "sha256:541c9af5b7a0ad23d15ec315a7da81150833c359f48124ed3789ff25eacd6f42", size = 12711024, upload-time = "2026-08-26T20:23:38.159Z" }, { url = "https://files.pythonhosted.org/packages/39/73/4542f829107468b5de4231af67f29927c093bfad11f3c1e5b2c08fb1206b/ty-0.0.75-py3-none-win_arm64.whl", hash = "sha256:541c9af5b7a0ad23d15ec315a7da81150833c359f48124ed3789ff25eacd6f42", size = 12711024, upload-time = "2026-08-26T20:23:38.159Z" },
] ]
[[package]]
name = "typer"
version = "0.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "rich" },
{ name = "shellingham" },
]
sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"