yoru-cli 0.1.0__tar.gz → 0.1.2__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.
Files changed (27) hide show
  1. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/.gitignore +63 -62
  2. yoru_cli-0.1.2/PKG-INFO +76 -0
  3. yoru_cli-0.1.2/README.md +50 -0
  4. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/pyproject.toml +44 -44
  5. yoru_cli-0.1.2/src/yoru_cli/__init__.py +6 -0
  6. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/src/yoru_cli/api.py +40 -40
  7. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/src/yoru_cli/cli.py +84 -84
  8. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/src/yoru_cli/doctor_cmd.py +92 -92
  9. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/src/yoru_cli/hook_template.py +119 -119
  10. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/src/yoru_cli/init_cmd.py +183 -183
  11. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/tests/test_doctor.py +18 -18
  12. yoru_cli-0.1.0/.claude/settings.local.json +0 -6
  13. yoru_cli-0.1.0/PKG-INFO +0 -61
  14. yoru_cli-0.1.0/README.md +0 -35
  15. yoru_cli-0.1.0/src/yoru_cli/__init__.py +0 -1
  16. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/LICENSE +0 -0
  17. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/src/yoru_cli/__main__.py +0 -0
  18. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/src/yoru_cli/config.py +0 -0
  19. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/src/yoru_cli/tail_cmd.py +0 -0
  20. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/src/yoru_cli/transcript_tailer.py +0 -0
  21. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/tests/__init__.py +0 -0
  22. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/tests/test_cli.py +0 -0
  23. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/tests/test_hook_template.py +0 -0
  24. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/tests/test_init.py +0 -0
  25. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/tests/test_init_settings_merge.py +0 -0
  26. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/tests/test_tail.py +0 -0
  27. {yoru_cli-0.1.0 → yoru_cli-0.1.2}/uv.lock +0 -0
@@ -1,65 +1,65 @@
1
- # overnight-saas root .gitignore
2
-
3
- # Python
4
- __pycache__/
5
- *.py[cod]
6
- *.pyc
7
- *.pyo
8
- *.egg-info/
9
- .Python
10
- .venv/
11
- venv/
12
- env/
13
- .pytest_cache/
14
- .ruff_cache/
15
- .mypy_cache/
16
- .coverage
17
- htmlcov/
18
- *.cover
19
-
20
- # Node / frontend
21
- node_modules/
22
- dist/
23
- build/
24
- .vite/
25
- *.log
26
- npm-debug.log*
27
- yarn-debug.log*
28
- yarn-error.log*
29
-
30
- # uv / pip
31
- uv.lock.tmp
32
- pip-wheel-metadata/
33
-
34
- # Env / secrets (NEVER commit)
35
- .env
36
- .env.*
37
- !.env.example
38
- !.env.template
39
-
40
- # Editors / OS
41
- .DS_Store
42
- Thumbs.db
43
- .idea/
44
- .vscode/
45
- *.swp
46
- *.swo
47
-
48
- # Docker local data
49
- docker/.data/
50
-
51
- # Receipt v0 local SQLite
52
- backend/data/*.db
53
- backend/data/*.db-*
54
- backend/data/backups/*.db
55
- backend/data/backups/*.db-*
56
-
57
- # Agent / vault ephemeral
58
- vault/STOP_NIGHT.off
59
- vault/*.off
60
-
61
- # Agent worktrees
62
- .worktrees/
1
+ # overnight-saas root .gitignore
2
+
3
+ # Python
4
+ __pycache__/
5
+ *.py[cod]
6
+ *.pyc
7
+ *.pyo
8
+ *.egg-info/
9
+ .Python
10
+ .venv/
11
+ venv/
12
+ env/
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .mypy_cache/
16
+ .coverage
17
+ htmlcov/
18
+ *.cover
19
+
20
+ # Node / frontend
21
+ node_modules/
22
+ dist/
23
+ build/
24
+ .vite/
25
+ *.log
26
+ npm-debug.log*
27
+ yarn-debug.log*
28
+ yarn-error.log*
29
+
30
+ # uv / pip
31
+ uv.lock.tmp
32
+ pip-wheel-metadata/
33
+
34
+ # Env / secrets (NEVER commit)
35
+ .env
36
+ .env.*
37
+ !.env.example
38
+ !.env.template
39
+
40
+ # Editors / OS
41
+ .DS_Store
42
+ Thumbs.db
43
+ .idea/
44
+ .vscode/
45
+ *.swp
46
+ *.swo
47
+
48
+ # Docker local data
49
+ docker/.data/
50
+
51
+ # Receipt v0 local SQLite
52
+ backend/data/*.db
53
+ backend/data/*.db-*
54
+ backend/data/backups/*.db
55
+ backend/data/backups/*.db-*
56
+
57
+ # Agent / vault ephemeral
58
+ vault/STOP_NIGHT.off
59
+ vault/*.off
60
+
61
+ # Agent worktrees
62
+ .worktrees/
63
63
 
64
64
  # Skill / agent working files — not code
65
65
  .agents/
@@ -69,3 +69,4 @@ backend/receipt.db
69
69
 
70
70
  # Runtime caches populated by pricing warmup etc.
71
71
  backend/data/*.json
72
+ .saasforge
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: yoru-cli
3
+ Version: 0.1.2
4
+ Summary: Yoru — audit-grade session receipts for autonomous AI coding agents.
5
+ Project-URL: Homepage, https://yoru.sh
6
+ Project-URL: Documentation, https://yoru.sh/docs
7
+ Project-URL: Repository, https://github.com/yoru-sh/cli
8
+ Project-URL: Issues, https://github.com/yoru-sh/cli/issues
9
+ Author-email: Yoru authors <hello@yoru.sh>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-agents,aider,audit,claude-code,cursor,observability
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx>=0.27
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # yoru-cli
28
+
29
+ The Claude Code hook installer for Yoru. One command installs a `PostToolUse` hook that forwards every tool call, file edit, and red-flag event to a Yoru backend. Your next session shows up in the dashboard automatically.
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/yoru-cli.svg)](https://pypi.org/project/yoru-cli/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install yoru-cli
38
+ yoru init
39
+ ```
40
+
41
+ `yoru init` opens a pairing URL in your browser, writes `~/.claude/hooks/yoru.sh`, and registers the hook in `~/.claude/settings.json`. By default it points at Yoru Cloud ([yoru.sh](https://yoru.sh)) — free forever for one developer.
42
+
43
+ Requires Python 3.10+. One runtime dep: `httpx`.
44
+
45
+ ## Usage
46
+
47
+ ```bash
48
+ yoru init # pair against https://api.yoru.sh
49
+ yoru init --server https://yoru.acme # pair against your self-hosted server
50
+ yoru init --token yoru_xxx --force # non-interactive (CI / scripted rotation)
51
+
52
+ yoru tail < events.jsonl # post a batch of events (debug)
53
+
54
+ yoru --version
55
+ yoru --help
56
+ ```
57
+
58
+ Exit codes: `0` ok · `1` already installed (use `--force`) · `2` auth failed · `3` 4xx · `4` 5xx/network.
59
+
60
+ ## How the hook works
61
+
62
+ The installed script is ~20 lines of Bash that `POST`s one JSON event per Claude Code tool call:
63
+
64
+ ```json
65
+ {"session_id": "…", "user": "…", "kind": "tool_use", "tool": "Bash", "content": "…"}
66
+ ```
67
+
68
+ It uses `curl --max-time 2 || true` so a Yoru outage never stalls your terminal — Claude Code doesn't notice the hook is even there.
69
+
70
+ ## Self-host the server
71
+
72
+ The CLI is MIT. The server (backend + dashboard) is AGPL-3.0 and lives at **[github.com/yoru-sh/yoru](https://github.com/yoru-sh/yoru)**. `docker-compose up`, then `yoru init --server https://your-host` — done.
73
+
74
+ ## License
75
+
76
+ [MIT](./LICENSE) · Copyright (c) 2026 Yoru authors. Issues and PRs welcome at [github.com/yoru-sh/cli](https://github.com/yoru-sh/cli).
@@ -0,0 +1,50 @@
1
+ # yoru-cli
2
+
3
+ The Claude Code hook installer for Yoru. One command installs a `PostToolUse` hook that forwards every tool call, file edit, and red-flag event to a Yoru backend. Your next session shows up in the dashboard automatically.
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/yoru-cli.svg)](https://pypi.org/project/yoru-cli/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install yoru-cli
12
+ yoru init
13
+ ```
14
+
15
+ `yoru init` opens a pairing URL in your browser, writes `~/.claude/hooks/yoru.sh`, and registers the hook in `~/.claude/settings.json`. By default it points at Yoru Cloud ([yoru.sh](https://yoru.sh)) — free forever for one developer.
16
+
17
+ Requires Python 3.10+. One runtime dep: `httpx`.
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ yoru init # pair against https://api.yoru.sh
23
+ yoru init --server https://yoru.acme # pair against your self-hosted server
24
+ yoru init --token yoru_xxx --force # non-interactive (CI / scripted rotation)
25
+
26
+ yoru tail < events.jsonl # post a batch of events (debug)
27
+
28
+ yoru --version
29
+ yoru --help
30
+ ```
31
+
32
+ Exit codes: `0` ok · `1` already installed (use `--force`) · `2` auth failed · `3` 4xx · `4` 5xx/network.
33
+
34
+ ## How the hook works
35
+
36
+ The installed script is ~20 lines of Bash that `POST`s one JSON event per Claude Code tool call:
37
+
38
+ ```json
39
+ {"session_id": "…", "user": "…", "kind": "tool_use", "tool": "Bash", "content": "…"}
40
+ ```
41
+
42
+ It uses `curl --max-time 2 || true` so a Yoru outage never stalls your terminal — Claude Code doesn't notice the hook is even there.
43
+
44
+ ## Self-host the server
45
+
46
+ The CLI is MIT. The server (backend + dashboard) is AGPL-3.0 and lives at **[github.com/yoru-sh/yoru](https://github.com/yoru-sh/yoru)**. `docker-compose up`, then `yoru init --server https://your-host` — done.
47
+
48
+ ## License
49
+
50
+ [MIT](./LICENSE) · Copyright (c) 2026 Yoru authors. Issues and PRs welcome at [github.com/yoru-sh/cli](https://github.com/yoru-sh/cli).
@@ -1,44 +1,44 @@
1
- [project]
2
- name = "yoru-cli"
3
- version = "0.1.0"
4
- description = "Yoru — audit-grade session receipts for autonomous AI coding agents."
5
- license = { text = "MIT" }
6
- license-files = ["LICENSE"]
7
- readme = "README.md"
8
- authors = [{ name = "Yoru authors", email = "hello@opentruth.ch" }]
9
- keywords = ["claude-code", "cursor", "aider", "ai-agents", "observability", "audit"]
10
- classifiers = [
11
- "License :: OSI Approved :: MIT License",
12
- "Programming Language :: Python :: 3",
13
- "Programming Language :: Python :: 3.10",
14
- "Programming Language :: Python :: 3.11",
15
- "Programming Language :: Python :: 3.12",
16
- "Environment :: Console",
17
- "Intended Audience :: Developers",
18
- "Topic :: Software Development :: Libraries :: Python Modules",
19
- ]
20
- requires-python = ">=3.10"
21
- dependencies = ["httpx>=0.27"]
22
-
23
- [project.urls]
24
- Homepage = "https://yoru.sh"
25
- Documentation = "https://yoru.sh/docs"
26
- Repository = "https://github.com/helios-code/overnight-saas"
27
- Issues = "https://github.com/helios-code/overnight-saas/issues"
28
-
29
- [project.scripts]
30
- yoru = "yoru_cli.cli:main"
31
-
32
- [project.optional-dependencies]
33
- dev = ["pytest>=7"]
34
-
35
- [build-system]
36
- requires = ["hatchling"]
37
- build-backend = "hatchling.build"
38
-
39
- [tool.hatch.build.targets.wheel]
40
- packages = ["src/yoru_cli"]
41
-
42
- [tool.pytest.ini_options]
43
- pythonpath = ["src"]
44
- testpaths = ["tests"]
1
+ [project]
2
+ name = "yoru-cli"
3
+ version = "0.1.2"
4
+ description = "Yoru — audit-grade session receipts for autonomous AI coding agents."
5
+ license = { text = "MIT" }
6
+ license-files = ["LICENSE"]
7
+ readme = "README.md"
8
+ authors = [{ name = "Yoru authors", email = "hello@yoru.sh" }]
9
+ keywords = ["claude-code", "cursor", "aider", "ai-agents", "observability", "audit"]
10
+ classifiers = [
11
+ "License :: OSI Approved :: MIT License",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.10",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ ]
20
+ requires-python = ">=3.10"
21
+ dependencies = ["httpx>=0.27"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://yoru.sh"
25
+ Documentation = "https://yoru.sh/docs"
26
+ Repository = "https://github.com/yoru-sh/cli"
27
+ Issues = "https://github.com/yoru-sh/cli/issues"
28
+
29
+ [project.scripts]
30
+ yoru = "yoru_cli.cli:main"
31
+
32
+ [project.optional-dependencies]
33
+ dev = ["pytest>=7"]
34
+
35
+ [build-system]
36
+ requires = ["hatchling"]
37
+ build-backend = "hatchling.build"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/yoru_cli"]
41
+
42
+ [tool.pytest.ini_options]
43
+ pythonpath = ["src"]
44
+ testpaths = ["tests"]
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("yoru-cli")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.0+dev"
@@ -1,40 +1,40 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- import httpx
6
-
7
-
8
- class ReceiptClient:
9
- def __init__(self, base_url: str, token: str | None = None) -> None:
10
- self.base_url = base_url.rstrip("/")
11
- self.token = token
12
-
13
- def start_device_code(self, label: str | None = None) -> dict[str, Any]:
14
- """Begin the device-pairing handshake — no auth needed."""
15
- r = httpx.post(
16
- f"{self.base_url}/api/v1/auth/device-code",
17
- json={"label": label} if label else {},
18
- timeout=5.0,
19
- )
20
- r.raise_for_status()
21
- return r.json()
22
-
23
- def poll_device_code(self, device_code: str) -> dict[str, Any]:
24
- """Poll for approval — returns {status, token?}."""
25
- r = httpx.post(
26
- f"{self.base_url}/api/v1/auth/device-code/poll",
27
- json={"device_code": device_code},
28
- timeout=10.0,
29
- )
30
- r.raise_for_status()
31
- return r.json()
32
-
33
- def post_events(self, events: list[dict[str, Any]]) -> httpx.Response:
34
- headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
35
- return httpx.post(
36
- f"{self.base_url}/api/v1/sessions/events",
37
- json={"events": events},
38
- headers=headers,
39
- timeout=5.0,
40
- )
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+
8
+ class ReceiptClient:
9
+ def __init__(self, base_url: str, token: str | None = None) -> None:
10
+ self.base_url = base_url.rstrip("/")
11
+ self.token = token
12
+
13
+ def start_device_code(self, label: str | None = None) -> dict[str, Any]:
14
+ """Begin the device-pairing handshake — no auth needed."""
15
+ r = httpx.post(
16
+ f"{self.base_url}/api/v1/auth/device-code",
17
+ json={"label": label} if label else {},
18
+ timeout=5.0,
19
+ )
20
+ r.raise_for_status()
21
+ return r.json()
22
+
23
+ def poll_device_code(self, device_code: str) -> dict[str, Any]:
24
+ """Poll for approval — returns {status, token?}."""
25
+ r = httpx.post(
26
+ f"{self.base_url}/api/v1/auth/device-code/poll",
27
+ json={"device_code": device_code},
28
+ timeout=10.0,
29
+ )
30
+ r.raise_for_status()
31
+ return r.json()
32
+
33
+ def post_events(self, events: list[dict[str, Any]]) -> httpx.Response:
34
+ headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
35
+ return httpx.post(
36
+ f"{self.base_url}/api/v1/sessions/events",
37
+ json={"events": events},
38
+ headers=headers,
39
+ timeout=5.0,
40
+ )
@@ -1,84 +1,84 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
-
5
- from . import __version__
6
- from . import config, doctor_cmd, init_cmd, tail_cmd
7
-
8
- DEFAULT_SERVER = "https://api.yoru.sh"
9
-
10
-
11
- def _build_parser() -> argparse.ArgumentParser:
12
- parser = argparse.ArgumentParser(
13
- prog="yoru",
14
- description="Yoru — audit-grade session receipts for autonomous AI coding agents.",
15
- )
16
- parser.add_argument(
17
- "--version",
18
- action="version",
19
- version=f"yoru {__version__}",
20
- )
21
-
22
- subparsers = parser.add_subparsers(dest="cmd", required=True, metavar="{init,tail,doctor}")
23
-
24
- p_init = subparsers.add_parser(
25
- "init",
26
- help="Install the Claude Code hook and write ~/.config/yoru/config.json.",
27
- )
28
- p_init.add_argument("--server", default=DEFAULT_SERVER, help=f"Backend URL (default: {DEFAULT_SERVER})")
29
- p_init.add_argument(
30
- "--token",
31
- default=None,
32
- help="Pre-minted hook token (rcpt_...) — for headless/CI/server setups. "
33
- "Also read from $YORU_TOKEN. Without this, yoru init launches "
34
- "interactive device pairing.",
35
- )
36
- p_init.add_argument(
37
- "--label",
38
- default=None,
39
- help="Human-readable machine label shown in the dashboard "
40
- "(default: <hostname> · <os>).",
41
- )
42
- p_init.add_argument(
43
- "--no-browser",
44
- action="store_true",
45
- help="Don't try to auto-open the pairing URL in a browser.",
46
- )
47
- p_init.add_argument("--force", action="store_true", help="Overwrite an existing install.")
48
-
49
- p_tail = subparsers.add_parser(
50
- "tail",
51
- help="Read JSON events from stdin and POST them as a batch (dev/debug).",
52
- )
53
- p_tail.add_argument(
54
- "--server",
55
- default=None,
56
- help=f"Backend URL (default: value from config, else {DEFAULT_SERVER}).",
57
- )
58
- p_tail.add_argument("--session-id", default=None, help="Session id to stamp on events missing one.")
59
-
60
- subparsers.add_parser(
61
- "doctor",
62
- help="Diagnose the install: config, backend, token, hook.",
63
- )
64
-
65
- return parser
66
-
67
-
68
- def main(argv: list[str] | None = None) -> int:
69
- parser = _build_parser()
70
- args = parser.parse_args(argv)
71
-
72
- if args.cmd == "tail" and args.server is None:
73
- cfg = config.load() or {}
74
- args.server = cfg.get("server", DEFAULT_SERVER)
75
-
76
- if args.cmd == "init":
77
- return init_cmd.run(args)
78
- if args.cmd == "tail":
79
- return tail_cmd.run(args)
80
- if args.cmd == "doctor":
81
- return doctor_cmd.run(args)
82
-
83
- parser.error(f"unknown command: {args.cmd!r}")
84
- return 2
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from . import __version__
6
+ from . import config, doctor_cmd, init_cmd, tail_cmd
7
+
8
+ DEFAULT_SERVER = "https://api.yoru.sh"
9
+
10
+
11
+ def _build_parser() -> argparse.ArgumentParser:
12
+ parser = argparse.ArgumentParser(
13
+ prog="yoru",
14
+ description="Yoru — audit-grade session receipts for autonomous AI coding agents.",
15
+ )
16
+ parser.add_argument(
17
+ "--version",
18
+ action="version",
19
+ version=f"yoru {__version__}",
20
+ )
21
+
22
+ subparsers = parser.add_subparsers(dest="cmd", required=True, metavar="{init,tail,doctor}")
23
+
24
+ p_init = subparsers.add_parser(
25
+ "init",
26
+ help="Install the Claude Code hook and write ~/.config/yoru/config.json.",
27
+ )
28
+ p_init.add_argument("--server", default=DEFAULT_SERVER, help=f"Backend URL (default: {DEFAULT_SERVER})")
29
+ p_init.add_argument(
30
+ "--token",
31
+ default=None,
32
+ help="Pre-minted hook token (rcpt_...) — for headless/CI/server setups. "
33
+ "Also read from $YORU_TOKEN. Without this, yoru init launches "
34
+ "interactive device pairing.",
35
+ )
36
+ p_init.add_argument(
37
+ "--label",
38
+ default=None,
39
+ help="Human-readable machine label shown in the dashboard "
40
+ "(default: <hostname> · <os>).",
41
+ )
42
+ p_init.add_argument(
43
+ "--no-browser",
44
+ action="store_true",
45
+ help="Don't try to auto-open the pairing URL in a browser.",
46
+ )
47
+ p_init.add_argument("--force", action="store_true", help="Overwrite an existing install.")
48
+
49
+ p_tail = subparsers.add_parser(
50
+ "tail",
51
+ help="Read JSON events from stdin and POST them as a batch (dev/debug).",
52
+ )
53
+ p_tail.add_argument(
54
+ "--server",
55
+ default=None,
56
+ help=f"Backend URL (default: value from config, else {DEFAULT_SERVER}).",
57
+ )
58
+ p_tail.add_argument("--session-id", default=None, help="Session id to stamp on events missing one.")
59
+
60
+ subparsers.add_parser(
61
+ "doctor",
62
+ help="Diagnose the install: config, backend, token, hook.",
63
+ )
64
+
65
+ return parser
66
+
67
+
68
+ def main(argv: list[str] | None = None) -> int:
69
+ parser = _build_parser()
70
+ args = parser.parse_args(argv)
71
+
72
+ if args.cmd == "tail" and args.server is None:
73
+ cfg = config.load() or {}
74
+ args.server = cfg.get("server", DEFAULT_SERVER)
75
+
76
+ if args.cmd == "init":
77
+ return init_cmd.run(args)
78
+ if args.cmd == "tail":
79
+ return tail_cmd.run(args)
80
+ if args.cmd == "doctor":
81
+ return doctor_cmd.run(args)
82
+
83
+ parser.error(f"unknown command: {args.cmd!r}")
84
+ return 2