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.
@@ -0,0 +1,6 @@
1
+ {
2
+ "enabledMcpjsonServers": [
3
+ "context7",
4
+ "agent-relay"
5
+ ]
6
+ }
@@ -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.
@@ -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).
@@ -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,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -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
+ ]