yoru-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.
- yoru_cli-0.1.0/.claude/settings.local.json +6 -0
- yoru_cli-0.1.0/.gitignore +71 -0
- yoru_cli-0.1.0/LICENSE +31 -0
- yoru_cli-0.1.0/PKG-INFO +61 -0
- yoru_cli-0.1.0/README.md +35 -0
- yoru_cli-0.1.0/pyproject.toml +44 -0
- yoru_cli-0.1.0/src/yoru_cli/__init__.py +1 -0
- yoru_cli-0.1.0/src/yoru_cli/__main__.py +3 -0
- yoru_cli-0.1.0/src/yoru_cli/api.py +40 -0
- yoru_cli-0.1.0/src/yoru_cli/cli.py +84 -0
- yoru_cli-0.1.0/src/yoru_cli/config.py +43 -0
- yoru_cli-0.1.0/src/yoru_cli/doctor_cmd.py +92 -0
- yoru_cli-0.1.0/src/yoru_cli/hook_template.py +119 -0
- yoru_cli-0.1.0/src/yoru_cli/init_cmd.py +183 -0
- yoru_cli-0.1.0/src/yoru_cli/tail_cmd.py +51 -0
- yoru_cli-0.1.0/src/yoru_cli/transcript_tailer.py +438 -0
- yoru_cli-0.1.0/tests/__init__.py +0 -0
- yoru_cli-0.1.0/tests/test_cli.py +35 -0
- yoru_cli-0.1.0/tests/test_doctor.py +18 -0
- yoru_cli-0.1.0/tests/test_hook_template.py +25 -0
- yoru_cli-0.1.0/tests/test_init.py +164 -0
- yoru_cli-0.1.0/tests/test_init_settings_merge.py +138 -0
- yoru_cli-0.1.0/tests/test_tail.py +110 -0
- yoru_cli-0.1.0/uv.lock +230 -0
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
|
|
64
|
+
# Skill / agent working files — not code
|
|
65
|
+
.agents/
|
|
66
|
+
|
|
67
|
+
# Stray SQLite at backend root (dev-time artifact, real DB lives at backend/data/)
|
|
68
|
+
backend/receipt.db
|
|
69
|
+
|
|
70
|
+
# Runtime caches populated by pricing warmup etc.
|
|
71
|
+
backend/data/*.json
|
yoru_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yoru authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
─────────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
SCOPE: The Yoru CLI — everything inside the `yoru-cli/` directory in
|
|
26
|
+
the Yoru monorepo. The CLI is intentionally MIT so it can be freely
|
|
27
|
+
embedded in proprietary dev environments, CI pipelines, internal tooling,
|
|
28
|
+
and closed-source forks without viral license obligations.
|
|
29
|
+
|
|
30
|
+
The SERVER (backend + dashboard + marketing + infra) is AGPL-3.0 — see the
|
|
31
|
+
`LICENSE` file at the repository root and `LICENSING.md` for the rationale.
|
yoru_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yoru-cli
|
|
3
|
+
Version: 0.1.0
|
|
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/helios-code/overnight-saas
|
|
8
|
+
Project-URL: Issues, https://github.com/helios-code/overnight-saas/issues
|
|
9
|
+
Author-email: Yoru authors <hello@opentruth.ch>
|
|
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
|
+
Yoru — audit-grade session receipts for autonomous AI coding agents.
|
|
30
|
+
|
|
31
|
+
One command installs a Claude Code hook that streams every tool call into the Yoru backend; the dashboard turns that feed into a signed session receipt.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install -e . # from the monorepo
|
|
37
|
+
# or once published:
|
|
38
|
+
# pip install yoru-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.10+. Only runtime dep is `httpx`.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
yoru init # writes ~/.claude/hooks/yoru.sh + ~/.config/yoru/config.json (0600)
|
|
47
|
+
yoru init --server http://localhost:8002 --user you@example.com # non-interactive (CI/smoke)
|
|
48
|
+
yoru init --server http://localhost:8002 --token rcpt_xxx --force
|
|
49
|
+
|
|
50
|
+
yoru tail # reads JSON events on stdin, POSTs them as a batch (dev/debug)
|
|
51
|
+
echo '{"session_id":"s1","user":"dev","kind":"tool_use","tool":"Bash"}' | yoru tail
|
|
52
|
+
|
|
53
|
+
receipt --version # receipt 0.1.0
|
|
54
|
+
receipt --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Exit codes: `0` ok, `1` already installed without `--force`, `2` auth failed, `3` 4xx, `4` 5xx/network.
|
|
58
|
+
|
|
59
|
+
## Spec
|
|
60
|
+
|
|
61
|
+
Frozen design doc: `vault/CLI-V0-DESIGN.md` in the monorepo (§1 layout, §2 pyproject, §3 subcommands, §4 hook shape, §5 auth, §7 event schema).
|
yoru_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# yoru-cli
|
|
2
|
+
|
|
3
|
+
Yoru — audit-grade session receipts for autonomous AI coding agents.
|
|
4
|
+
|
|
5
|
+
One command installs a Claude Code hook that streams every tool call into the Yoru backend; the dashboard turns that feed into a signed session receipt.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e . # from the monorepo
|
|
11
|
+
# or once published:
|
|
12
|
+
# pip install yoru-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.10+. Only runtime dep is `httpx`.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
yoru init # writes ~/.claude/hooks/yoru.sh + ~/.config/yoru/config.json (0600)
|
|
21
|
+
yoru init --server http://localhost:8002 --user you@example.com # non-interactive (CI/smoke)
|
|
22
|
+
yoru init --server http://localhost:8002 --token rcpt_xxx --force
|
|
23
|
+
|
|
24
|
+
yoru tail # reads JSON events on stdin, POSTs them as a batch (dev/debug)
|
|
25
|
+
echo '{"session_id":"s1","user":"dev","kind":"tool_use","tool":"Bash"}' | yoru tail
|
|
26
|
+
|
|
27
|
+
receipt --version # receipt 0.1.0
|
|
28
|
+
receipt --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Exit codes: `0` ok, `1` already installed without `--force`, `2` auth failed, `3` 4xx, `4` 5xx/network.
|
|
32
|
+
|
|
33
|
+
## Spec
|
|
34
|
+
|
|
35
|
+
Frozen design doc: `vault/CLI-V0-DESIGN.md` in the monorepo (§1 layout, §2 pyproject, §3 subcommands, §4 hook shape, §5 auth, §7 event schema).
|
|
@@ -0,0 +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"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +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
|
+
)
|
|
@@ -0,0 +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
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _config_dir() -> Path:
|
|
11
|
+
return Path.home() / ".config" / "yoru"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _config_file() -> Path:
|
|
15
|
+
return _config_dir() / "config.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CONFIG_DIR: Path = _config_dir()
|
|
19
|
+
CONFIG_FILE: Path = _config_file()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def exists() -> bool:
|
|
23
|
+
return _config_file().is_file()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load() -> dict[str, Any] | None:
|
|
27
|
+
path = _config_file()
|
|
28
|
+
if not path.is_file():
|
|
29
|
+
return None
|
|
30
|
+
with path.open("r", encoding="utf-8") as f:
|
|
31
|
+
return json.load(f)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save(data: dict[str, Any]) -> None:
|
|
35
|
+
dir_path = _config_dir()
|
|
36
|
+
file_path = _config_file()
|
|
37
|
+
os.makedirs(dir_path, mode=0o700, exist_ok=True)
|
|
38
|
+
payload = dict(data)
|
|
39
|
+
payload.setdefault("created_at", datetime.now(timezone.utc).isoformat())
|
|
40
|
+
fd = os.open(file_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
41
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
42
|
+
json.dump(payload, f, indent=2, sort_keys=True)
|
|
43
|
+
f.write("\n")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""`yoru doctor` — diagnostic subcommand.
|
|
2
|
+
|
|
3
|
+
Read-only check of the install:
|
|
4
|
+
1. config.json present → else exit 1
|
|
5
|
+
2. backend /health/ready reachable → else exit 2
|
|
6
|
+
3. hook-token valid (GET /hook-tokens) → else exit 3 on 401
|
|
7
|
+
4. ~/.claude/hooks/yoru.sh is 0755 → else exit 4
|
|
8
|
+
|
|
9
|
+
Prints ✓ lines to stdout on success (exit 0). Failures go to stderr with a
|
|
10
|
+
short human-readable reason. No fixes attempted.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import os
|
|
16
|
+
import stat
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
from . import config
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _token_suffix(token: str) -> str:
|
|
26
|
+
tail = token[-4:] if len(token) >= 4 else token
|
|
27
|
+
return f"rcpt_...{tail}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _hook_path() -> Path:
|
|
31
|
+
return Path.home() / ".claude" / "hooks" / "yoru.sh"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run(args: argparse.Namespace) -> int: # noqa: ARG001 — argparse hands args, unused for now
|
|
35
|
+
# 1. config
|
|
36
|
+
cfg = config.load()
|
|
37
|
+
if cfg is None:
|
|
38
|
+
print("yoru init not run", file=sys.stderr)
|
|
39
|
+
return 1
|
|
40
|
+
server = (cfg.get("server") or "").rstrip("/")
|
|
41
|
+
token = cfg.get("token") or ""
|
|
42
|
+
if not server or not token:
|
|
43
|
+
print("yoru init not run", file=sys.stderr)
|
|
44
|
+
return 1
|
|
45
|
+
|
|
46
|
+
# 2. backend /health/ready
|
|
47
|
+
try:
|
|
48
|
+
r = httpx.get(f"{server}/health/ready", timeout=5.0)
|
|
49
|
+
except httpx.HTTPError:
|
|
50
|
+
print(f"backend unreachable at {server}", file=sys.stderr)
|
|
51
|
+
return 2
|
|
52
|
+
if r.status_code != 200:
|
|
53
|
+
print(f"backend unreachable at {server}", file=sys.stderr)
|
|
54
|
+
return 2
|
|
55
|
+
|
|
56
|
+
# 3. hook-token validity
|
|
57
|
+
try:
|
|
58
|
+
r = httpx.get(
|
|
59
|
+
f"{server}/api/v1/auth/hook-tokens",
|
|
60
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
61
|
+
timeout=5.0,
|
|
62
|
+
)
|
|
63
|
+
except httpx.HTTPError:
|
|
64
|
+
print(f"backend unreachable at {server}", file=sys.stderr)
|
|
65
|
+
return 2
|
|
66
|
+
if r.status_code == 401:
|
|
67
|
+
print("token revoked or expired", file=sys.stderr)
|
|
68
|
+
return 3
|
|
69
|
+
if r.status_code != 200:
|
|
70
|
+
print(
|
|
71
|
+
f"hook-token check failed: HTTP {r.status_code}",
|
|
72
|
+
file=sys.stderr,
|
|
73
|
+
)
|
|
74
|
+
return 3
|
|
75
|
+
|
|
76
|
+
# 4. hook file + perms
|
|
77
|
+
hook = _hook_path()
|
|
78
|
+
if not hook.is_file():
|
|
79
|
+
print("hook file missing or not 0755", file=sys.stderr)
|
|
80
|
+
return 4
|
|
81
|
+
mode = stat.S_IMODE(hook.stat().st_mode)
|
|
82
|
+
if mode != 0o755:
|
|
83
|
+
print("hook file missing or not 0755", file=sys.stderr)
|
|
84
|
+
return 4
|
|
85
|
+
|
|
86
|
+
# all green
|
|
87
|
+
user = cfg.get("user") or "authenticated"
|
|
88
|
+
print(f"\u2713 config at ~/.config/yoru/config.json (token {_token_suffix(token)})")
|
|
89
|
+
print(f"\u2713 backend {server} reachable")
|
|
90
|
+
print(f"\u2713 hook-token valid (user: {user})")
|
|
91
|
+
print("\u2713 hook installed at ~/.claude/hooks/yoru.sh")
|
|
92
|
+
return 0
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Bundled Claude Code hook script — written verbatim by `yoru init`.
|
|
2
|
+
|
|
3
|
+
Shape is frozen in vault/CLI-V0-DESIGN.md §4. Bash (not Python) for fast startup;
|
|
4
|
+
`curl --max-time 2 || true` keeps the hook from ever blocking the agent.
|
|
5
|
+
v0 posts one event per tool call — batching is a v1 optimization.
|
|
6
|
+
|
|
7
|
+
Subscribed hook events (configured in ~/.claude/settings.json):
|
|
8
|
+
- SessionStart → kind=session_start
|
|
9
|
+
- UserPromptSubmit → kind=message (prompt text captured)
|
|
10
|
+
- PostToolUse → kind inferred (tool_use | file_change)
|
|
11
|
+
- Notification → kind=message (permission/input prompts)
|
|
12
|
+
- Stop → kind=session_end
|
|
13
|
+
- SubagentStop → kind=message (lightweight, doesn't close session)
|
|
14
|
+
|
|
15
|
+
PreToolUse is NOT subscribed — PostToolUse carries the same tool_input plus
|
|
16
|
+
tool_response, so subscribing to both doubles traffic for no gain.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
HOOK_SCRIPT: str = """#!/usr/bin/env bash
|
|
20
|
+
# Claude Code hook — Receipt ingest. Handles all subscribed hook events.
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
# Skip events when AGENT_RELAY_CHILD=1 (agent-relay-spawned children — prevents dashboard noise)
|
|
23
|
+
[ "${AGENT_RELAY_CHILD:-0}" = "1" ] && exit 0
|
|
24
|
+
CFG="${HOME}/.config/yoru/config.json"
|
|
25
|
+
[ -r "$CFG" ] || exit 0 # silent no-op if uninstalled
|
|
26
|
+
SERVER=$(python3 -c 'import json,os;print(json.load(open(os.path.expanduser("~/.config/yoru/config.json")))["server"])')
|
|
27
|
+
TOKEN=$(python3 -c 'import json,os;print(json.load(open(os.path.expanduser("~/.config/yoru/config.json")))["token"])')
|
|
28
|
+
# Claude Code pipes the hook event as JSON on stdin. We parse the original
|
|
29
|
+
# payload, attach it verbatim to `raw` (so the backend sees tool_input /
|
|
30
|
+
# tool_response — Pydantic drops unknown top-level fields otherwise), then
|
|
31
|
+
# mutate the top-level envelope with `kind` + extracted `content` for the
|
|
32
|
+
# renderer-friendly shape.
|
|
33
|
+
#
|
|
34
|
+
# Routing context (Phase C): hook detects cwd + git remote/branch and ships
|
|
35
|
+
# them with every event so the server can route the session to the right
|
|
36
|
+
# workspace. `git` calls are cached per-session in $TMPDIR to keep the hook
|
|
37
|
+
# fast on hot PostToolUse paths.
|
|
38
|
+
BODY=$(python3 -c 'import sys,json,os,subprocess
|
|
39
|
+
original=json.loads(sys.stdin.read())
|
|
40
|
+
e=dict(original)
|
|
41
|
+
e["raw"]=original
|
|
42
|
+
|
|
43
|
+
# Routing context — cwd from Claude payload (reliable), git info cached.
|
|
44
|
+
cwd = original.get("cwd")
|
|
45
|
+
if isinstance(cwd, str) and cwd:
|
|
46
|
+
e["cwd"] = cwd
|
|
47
|
+
|
|
48
|
+
sess_id = original.get("session_id") or original.get("sessionId") or ""
|
|
49
|
+
cache_dir = os.environ.get("TMPDIR", "/tmp")
|
|
50
|
+
cache = os.path.join(cache_dir, f".receipt-ctx-{sess_id}.env") if sess_id else None
|
|
51
|
+
git_remote = None
|
|
52
|
+
git_branch = None
|
|
53
|
+
if cache and os.path.exists(cache):
|
|
54
|
+
try:
|
|
55
|
+
for line in open(cache, encoding="utf-8"):
|
|
56
|
+
k,_,v = line.strip().partition("=")
|
|
57
|
+
if k == "git_remote": git_remote = v or None
|
|
58
|
+
elif k == "git_branch": git_branch = v or None
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
if (git_remote is None or git_branch is None) and isinstance(cwd, str) and cwd:
|
|
62
|
+
def _run(args):
|
|
63
|
+
try:
|
|
64
|
+
return subprocess.check_output(args, cwd=cwd, stderr=subprocess.DEVNULL, timeout=1).decode().strip() or None
|
|
65
|
+
except Exception:
|
|
66
|
+
return None
|
|
67
|
+
if git_remote is None:
|
|
68
|
+
git_remote = _run(["git","remote","get-url","origin"])
|
|
69
|
+
if git_branch is None:
|
|
70
|
+
git_branch = _run(["git","rev-parse","--abbrev-ref","HEAD"])
|
|
71
|
+
if cache:
|
|
72
|
+
try:
|
|
73
|
+
with open(cache, "w", encoding="utf-8") as f:
|
|
74
|
+
if git_remote: f.write(f"git_remote={git_remote}\\n")
|
|
75
|
+
if git_branch: f.write(f"git_branch={git_branch}\\n")
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
if git_remote: e["git_remote"] = git_remote
|
|
79
|
+
if git_branch: e["git_branch"] = git_branch
|
|
80
|
+
|
|
81
|
+
hen=e.get("hook_event_name")
|
|
82
|
+
if hen=="SessionStart":
|
|
83
|
+
e["kind"]="session_start"
|
|
84
|
+
elif hen=="UserPromptSubmit":
|
|
85
|
+
e["kind"]="message"
|
|
86
|
+
e["tool"]="user"
|
|
87
|
+
p=e.get("prompt")
|
|
88
|
+
if isinstance(p,str) and p: e["content"]=p[:2000]
|
|
89
|
+
elif hen=="Notification":
|
|
90
|
+
e["kind"]="message"
|
|
91
|
+
e["tool"]="notification"
|
|
92
|
+
m=e.get("message")
|
|
93
|
+
if isinstance(m,str) and m: e["content"]=m[:2000]
|
|
94
|
+
elif hen=="SubagentStop":
|
|
95
|
+
e["kind"]="message"
|
|
96
|
+
e["tool"]="subagent"
|
|
97
|
+
e["content"]="subagent stopped"
|
|
98
|
+
elif hen=="Stop":
|
|
99
|
+
e["kind"]="session_end"
|
|
100
|
+
# PostToolUse / PreToolUse: leave kind unset → backend _infer_kind() from tool
|
|
101
|
+
print(json.dumps({"events":[e]}))')
|
|
102
|
+
curl -sS --max-time 2 -X POST "${SERVER}/api/v1/sessions/events" \\
|
|
103
|
+
-H "Authorization: Bearer ${TOKEN}" \\
|
|
104
|
+
-H "Content-Type: application/json" \\
|
|
105
|
+
-d "${BODY}" >/dev/null 2>&1 || true # never block the agent
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Hook subscriptions to write into ~/.claude/settings.json.
|
|
110
|
+
# Each entry is a (hook_event_name, description) pair; the installer writes a
|
|
111
|
+
# single `matcher:"*"` entry per event pointing at ~/.claude/hooks/yoru.sh.
|
|
112
|
+
HOOK_SUBSCRIPTIONS: list[tuple[str, str]] = [
|
|
113
|
+
("SessionStart", "capture session boundary"),
|
|
114
|
+
("UserPromptSubmit", "capture user prompts (message events)"),
|
|
115
|
+
("PostToolUse", "capture tool_use + file_change"),
|
|
116
|
+
("Notification", "capture permission/input prompts"),
|
|
117
|
+
("Stop", "capture session close"),
|
|
118
|
+
("SubagentStop", "capture subagent lifecycle"),
|
|
119
|
+
]
|