trajrl 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.
trajrl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: trajrl
3
+ Version: 0.1.0
4
+ Summary: CLI for TrajectoryRL, Bittensor subnet 11 — agent-friendly access to live validator, miner, and evaluation data.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: typer>=0.9
9
+ Requires-Dist: httpx>=0.25
10
+ Requires-Dist: rich>=13.0
11
+
12
+ # trajrl
13
+
14
+ CLI for the [TrajectoryRL subnet](https://trajrl.com) (Bittensor SN11). Query live validator, miner, and evaluation data from the terminal.
15
+
16
+ Designed for AI agents (Claude Code, Cursor) and humans alike — outputs JSON when piped, Rich tables when interactive.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install trajrl
22
+ ```
23
+
24
+ ## Commands
25
+
26
+ ```
27
+ trajrl status # Network health overview
28
+ trajrl validators # List all validators
29
+ trajrl scores <validator_hotkey> # Per-miner scores from a validator
30
+ trajrl miner <hotkey> # Miner detail + diagnostics
31
+ trajrl pack <hotkey> <pack_hash> # Pack evaluation detail
32
+ trajrl submissions [--failed] # Recent pack submissions
33
+ trajrl logs [--type cycle|miner] # Eval log archives
34
+ ```
35
+
36
+ ### Global Options
37
+
38
+ Every command accepts:
39
+
40
+ | Option | Description |
41
+ |--------|-------------|
42
+ | `--json` / `-j` | Force JSON output (auto-enabled when stdout is piped) |
43
+ | `--base-url URL` | Override API base (default: `https://trajrl.com`, env: `TRAJRL_BASE_URL`) |
44
+
45
+ ## Usage Examples
46
+
47
+ ### Quick network check
48
+
49
+ ```bash
50
+ trajrl status
51
+ ```
52
+ ```
53
+ ╭──────────────────── Network Status ────────────────────╮
54
+ │ Validators: 7 total, 7 active (seen <1h) │
55
+ │ LLM Models: zhipu/glm-5 (3), chutes/GLM-5-TEE (3) │
56
+ │ Latest Eval: 7h ago │
57
+ │ Submissions: 65 passed, 35 failed (last batch) │
58
+ ╰────────────────────────────────────────────────────────╯
59
+ ```
60
+
61
+ ### List validators
62
+
63
+ ```bash
64
+ trajrl validators
65
+ ```
66
+ ```
67
+ Hotkey UID Version LLM Model Last Eval Last Seen
68
+ 5Cd6h…sn11 29 0.2.7 chutes/zai-org/GLM-5… 7h ago 2m ago
69
+ 5EcgNd…797f 221 0.2.7 zhipu/glm-5 10h ago 6m ago
70
+ ...
71
+ ```
72
+
73
+ ### Inspect a miner
74
+
75
+ ```bash
76
+ trajrl miner 5HMgR6LnNqUAtaKRwa6bLF4Vy4KBf7TaxCLehyff9mWPhSHt
77
+ ```
78
+
79
+ Shows rank, qualification status, cost, scenario breakdown, per-validator reports, recent submissions, and ban records.
80
+
81
+ ### View failed submissions
82
+
83
+ ```bash
84
+ trajrl submissions --failed
85
+ ```
86
+
87
+ ### Filter eval logs
88
+
89
+ ```bash
90
+ trajrl logs --type cycle --limit 5
91
+ trajrl logs --validator 5Cd6h... --type miner
92
+ trajrl logs --eval-id 20260324_000340
93
+ ```
94
+
95
+ ### JSON output for agents
96
+
97
+ Pipe to any tool — JSON is automatic:
98
+
99
+ ```bash
100
+ trajrl validators | jq '.validators[].hotkey'
101
+ trajrl scores 5Cd6h... --json | python3 -c "
102
+ import sys, json
103
+ d = json.load(sys.stdin)
104
+ for e in d['entries'][:5]:
105
+ print(f\"{e['minerHotkey'][:12]} qual={e['qualified']} cost={e['costUsd']}\")
106
+ "
107
+ ```
108
+
109
+ Force JSON in an interactive terminal:
110
+
111
+ ```bash
112
+ trajrl miner 5HMgR6... --json
113
+ ```
114
+
115
+ ## API Reference
116
+
117
+ All data comes from the [TrajectoryRL Public API](https://trajrl.com) — read-only, no authentication required.
118
+
119
+ | Endpoint | CLI Command |
120
+ |----------|-------------|
121
+ | `GET /api/validators` | `trajrl validators` |
122
+ | `GET /api/scores/by-validator?validator=` | `trajrl scores <hotkey>` |
123
+ | `GET /api/miners/:hotkey` | `trajrl miner <hotkey>` |
124
+ | `GET /api/miners/:hotkey/packs/:hash` | `trajrl pack <hotkey> <hash>` |
125
+ | `GET /api/submissions` | `trajrl submissions` |
126
+ | `GET /api/eval-logs` | `trajrl logs` |
127
+
128
+ ## Development
129
+
130
+ ```bash
131
+ git clone <repo> && cd trajrl
132
+ pip install -e .
133
+ trajrl --help
134
+ ```
trajrl-0.1.0/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # trajrl
2
+
3
+ CLI for the [TrajectoryRL subnet](https://trajrl.com) (Bittensor SN11). Query live validator, miner, and evaluation data from the terminal.
4
+
5
+ Designed for AI agents (Claude Code, Cursor) and humans alike — outputs JSON when piped, Rich tables when interactive.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install trajrl
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ```
16
+ trajrl status # Network health overview
17
+ trajrl validators # List all validators
18
+ trajrl scores <validator_hotkey> # Per-miner scores from a validator
19
+ trajrl miner <hotkey> # Miner detail + diagnostics
20
+ trajrl pack <hotkey> <pack_hash> # Pack evaluation detail
21
+ trajrl submissions [--failed] # Recent pack submissions
22
+ trajrl logs [--type cycle|miner] # Eval log archives
23
+ ```
24
+
25
+ ### Global Options
26
+
27
+ Every command accepts:
28
+
29
+ | Option | Description |
30
+ |--------|-------------|
31
+ | `--json` / `-j` | Force JSON output (auto-enabled when stdout is piped) |
32
+ | `--base-url URL` | Override API base (default: `https://trajrl.com`, env: `TRAJRL_BASE_URL`) |
33
+
34
+ ## Usage Examples
35
+
36
+ ### Quick network check
37
+
38
+ ```bash
39
+ trajrl status
40
+ ```
41
+ ```
42
+ ╭──────────────────── Network Status ────────────────────╮
43
+ │ Validators: 7 total, 7 active (seen <1h) │
44
+ │ LLM Models: zhipu/glm-5 (3), chutes/GLM-5-TEE (3) │
45
+ │ Latest Eval: 7h ago │
46
+ │ Submissions: 65 passed, 35 failed (last batch) │
47
+ ╰────────────────────────────────────────────────────────╯
48
+ ```
49
+
50
+ ### List validators
51
+
52
+ ```bash
53
+ trajrl validators
54
+ ```
55
+ ```
56
+ Hotkey UID Version LLM Model Last Eval Last Seen
57
+ 5Cd6h…sn11 29 0.2.7 chutes/zai-org/GLM-5… 7h ago 2m ago
58
+ 5EcgNd…797f 221 0.2.7 zhipu/glm-5 10h ago 6m ago
59
+ ...
60
+ ```
61
+
62
+ ### Inspect a miner
63
+
64
+ ```bash
65
+ trajrl miner 5HMgR6LnNqUAtaKRwa6bLF4Vy4KBf7TaxCLehyff9mWPhSHt
66
+ ```
67
+
68
+ Shows rank, qualification status, cost, scenario breakdown, per-validator reports, recent submissions, and ban records.
69
+
70
+ ### View failed submissions
71
+
72
+ ```bash
73
+ trajrl submissions --failed
74
+ ```
75
+
76
+ ### Filter eval logs
77
+
78
+ ```bash
79
+ trajrl logs --type cycle --limit 5
80
+ trajrl logs --validator 5Cd6h... --type miner
81
+ trajrl logs --eval-id 20260324_000340
82
+ ```
83
+
84
+ ### JSON output for agents
85
+
86
+ Pipe to any tool — JSON is automatic:
87
+
88
+ ```bash
89
+ trajrl validators | jq '.validators[].hotkey'
90
+ trajrl scores 5Cd6h... --json | python3 -c "
91
+ import sys, json
92
+ d = json.load(sys.stdin)
93
+ for e in d['entries'][:5]:
94
+ print(f\"{e['minerHotkey'][:12]} qual={e['qualified']} cost={e['costUsd']}\")
95
+ "
96
+ ```
97
+
98
+ Force JSON in an interactive terminal:
99
+
100
+ ```bash
101
+ trajrl miner 5HMgR6... --json
102
+ ```
103
+
104
+ ## API Reference
105
+
106
+ All data comes from the [TrajectoryRL Public API](https://trajrl.com) — read-only, no authentication required.
107
+
108
+ | Endpoint | CLI Command |
109
+ |----------|-------------|
110
+ | `GET /api/validators` | `trajrl validators` |
111
+ | `GET /api/scores/by-validator?validator=` | `trajrl scores <hotkey>` |
112
+ | `GET /api/miners/:hotkey` | `trajrl miner <hotkey>` |
113
+ | `GET /api/miners/:hotkey/packs/:hash` | `trajrl pack <hotkey> <hash>` |
114
+ | `GET /api/submissions` | `trajrl submissions` |
115
+ | `GET /api/eval-logs` | `trajrl logs` |
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ git clone <repo> && cd trajrl
121
+ pip install -e .
122
+ trajrl --help
123
+ ```
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "trajrl"
7
+ version = "0.1.0"
8
+ description = "CLI for TrajectoryRL, Bittensor subnet 11 — agent-friendly access to live validator, miner, and evaluation data."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ dependencies = [
13
+ "typer>=0.9",
14
+ "httpx>=0.25",
15
+ "rich>=13.0",
16
+ ]
17
+
18
+ [project.scripts]
19
+ trajrl = "trajrl.cli:app"
trajrl-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,85 @@
1
+ """Typed HTTP client for the TrajectoryRL public API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ DEFAULT_BASE_URL = "https://trajrl.com"
11
+ _TIMEOUT = 30.0
12
+
13
+
14
+ @dataclass
15
+ class TrajRLClient:
16
+ base_url: str = DEFAULT_BASE_URL
17
+ _client: httpx.Client = field(init=False, repr=False)
18
+
19
+ def __post_init__(self) -> None:
20
+ self._client = httpx.Client(
21
+ base_url=self.base_url.rstrip("/"),
22
+ timeout=_TIMEOUT,
23
+ headers={"Accept": "application/json"},
24
+ )
25
+
26
+ # -- endpoints ---------------------------------------------------------
27
+
28
+ def validators(self) -> dict[str, Any]:
29
+ """GET /api/validators"""
30
+ return self._get("/api/validators")
31
+
32
+ def scores_by_validator(self, validator: str) -> dict[str, Any]:
33
+ """GET /api/scores/by-validator?validator=<hotkey>"""
34
+ return self._get("/api/scores/by-validator", params={"validator": validator})
35
+
36
+ def miner(self, hotkey: str) -> dict[str, Any]:
37
+ """GET /api/miners/:hotkey"""
38
+ return self._get(f"/api/miners/{hotkey}")
39
+
40
+ def pack(self, hotkey: str, pack_hash: str) -> dict[str, Any]:
41
+ """GET /api/miners/:hotkey/packs/:packHash"""
42
+ return self._get(f"/api/miners/{hotkey}/packs/{pack_hash}")
43
+
44
+ def submissions(self, limit: int | None = None) -> dict[str, Any]:
45
+ """GET /api/submissions"""
46
+ return self._get("/api/submissions", params=_compact({"limit": limit}))
47
+
48
+ def eval_logs(
49
+ self,
50
+ *,
51
+ validator: str | None = None,
52
+ miner: str | None = None,
53
+ log_type: str | None = None,
54
+ eval_id: str | None = None,
55
+ pack_hash: str | None = None,
56
+ from_date: str | None = None,
57
+ to_date: str | None = None,
58
+ limit: int | None = None,
59
+ offset: int | None = None,
60
+ ) -> dict[str, Any]:
61
+ """GET /api/eval-logs"""
62
+ params = _compact({
63
+ "validator": validator,
64
+ "miner": miner,
65
+ "type": log_type,
66
+ "eval_id": eval_id,
67
+ "pack_hash": pack_hash,
68
+ "from": from_date,
69
+ "to": to_date,
70
+ "limit": limit,
71
+ "offset": offset,
72
+ })
73
+ return self._get("/api/eval-logs", params=params)
74
+
75
+ # -- internal ----------------------------------------------------------
76
+
77
+ def _get(self, path: str, params: dict | None = None) -> dict[str, Any]:
78
+ resp = self._client.get(path, params=params)
79
+ resp.raise_for_status()
80
+ return resp.json()
81
+
82
+
83
+ def _compact(d: dict) -> dict:
84
+ """Remove None values from a dict."""
85
+ return {k: v for k, v in d.items() if v is not None}
@@ -0,0 +1,151 @@
1
+ """trajrl — CLI for the TrajectoryRL subnet."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from typing import Annotated, Optional
8
+
9
+ import typer
10
+
11
+ from trajrl.api import TrajRLClient
12
+ from trajrl import display as fmt
13
+
14
+ app = typer.Typer(
15
+ name="trajrl",
16
+ help="CLI for the TrajectoryRL subnet — query live validator, miner, and evaluation data.",
17
+ no_args_is_help=True,
18
+ pretty_exceptions_enable=False,
19
+ )
20
+
21
+ # -- shared option defaults ------------------------------------------------
22
+
23
+ _json_opt = typer.Option("--json", "-j", help="Force JSON output (auto when piped).")
24
+ _base_url_opt = typer.Option("--base-url", help="API base URL.", envvar="TRAJRL_BASE_URL")
25
+
26
+
27
+ def _client(base_url: str) -> TrajRLClient:
28
+ return TrajRLClient(base_url=base_url)
29
+
30
+
31
+ def _want_json(flag: bool) -> bool:
32
+ return flag or not sys.stdout.isatty()
33
+
34
+
35
+ def _print_json(data: dict) -> None:
36
+ print(json.dumps(data, indent=2, ensure_ascii=False))
37
+
38
+
39
+ # -- commands --------------------------------------------------------------
40
+
41
+ @app.command()
42
+ def status(
43
+ json_output: Annotated[bool, _json_opt] = False,
44
+ base_url: Annotated[str, _base_url_opt] = "https://trajrl.com",
45
+ ) -> None:
46
+ """Network health overview — validators, submissions, models."""
47
+ client = _client(base_url)
48
+ vali_data = client.validators()
49
+ subs_data = client.submissions()
50
+ if _want_json(json_output):
51
+ _print_json({"validators": vali_data, "submissions": subs_data})
52
+ else:
53
+ fmt.display_status(vali_data, subs_data)
54
+
55
+
56
+ @app.command()
57
+ def validators(
58
+ json_output: Annotated[bool, _json_opt] = False,
59
+ base_url: Annotated[str, _base_url_opt] = "https://trajrl.com",
60
+ ) -> None:
61
+ """List all validators with heartbeat status and LLM model."""
62
+ data = _client(base_url).validators()
63
+ if _want_json(json_output):
64
+ _print_json(data)
65
+ else:
66
+ fmt.display_validators(data)
67
+
68
+
69
+ @app.command()
70
+ def scores(
71
+ validator: Annotated[str, typer.Argument(help="Validator SS58 hotkey.")],
72
+ json_output: Annotated[bool, _json_opt] = False,
73
+ base_url: Annotated[str, _base_url_opt] = "https://trajrl.com",
74
+ ) -> None:
75
+ """Per-miner evaluation scores from a specific validator."""
76
+ data = _client(base_url).scores_by_validator(validator)
77
+ if _want_json(json_output):
78
+ _print_json(data)
79
+ else:
80
+ fmt.display_scores(data)
81
+
82
+
83
+ @app.command()
84
+ def miner(
85
+ hotkey: Annotated[str, typer.Argument(help="Miner SS58 hotkey.")],
86
+ json_output: Annotated[bool, _json_opt] = False,
87
+ base_url: Annotated[str, _base_url_opt] = "https://trajrl.com",
88
+ ) -> None:
89
+ """Detailed evaluation data for a specific miner."""
90
+ data = _client(base_url).miner(hotkey)
91
+ if _want_json(json_output):
92
+ _print_json(data)
93
+ else:
94
+ fmt.display_miner(data)
95
+
96
+
97
+ @app.command()
98
+ def pack(
99
+ hotkey: Annotated[str, typer.Argument(help="Miner SS58 hotkey.")],
100
+ pack_hash: Annotated[str, typer.Argument(help="Pack SHA-256 hash.")],
101
+ json_output: Annotated[bool, _json_opt] = False,
102
+ base_url: Annotated[str, _base_url_opt] = "https://trajrl.com",
103
+ ) -> None:
104
+ """Evaluation data for a specific miner's pack."""
105
+ data = _client(base_url).pack(hotkey, pack_hash)
106
+ if _want_json(json_output):
107
+ _print_json(data)
108
+ else:
109
+ fmt.display_pack(data)
110
+
111
+
112
+ @app.command()
113
+ def submissions(
114
+ failed: Annotated[bool, typer.Option("--failed", help="Show only failed submissions.")] = False,
115
+ json_output: Annotated[bool, _json_opt] = False,
116
+ base_url: Annotated[str, _base_url_opt] = "https://trajrl.com",
117
+ ) -> None:
118
+ """Recent pack submissions (passed and failed)."""
119
+ data = _client(base_url).submissions()
120
+ if failed:
121
+ data["submissions"] = [s for s in data.get("submissions", []) if s.get("evalStatus") == "failed"]
122
+ if _want_json(json_output):
123
+ _print_json(data)
124
+ else:
125
+ fmt.display_submissions(data, failed_only=failed)
126
+
127
+
128
+ @app.command()
129
+ def logs(
130
+ validator: Annotated[Optional[str], typer.Option("--validator", "-v", help="Filter by validator hotkey.")] = None,
131
+ miner_key: Annotated[Optional[str], typer.Option("--miner", "-m", help="Filter by miner hotkey.")] = None,
132
+ log_type: Annotated[Optional[str], typer.Option("--type", "-t", help="Log type: 'miner' or 'cycle'.")] = None,
133
+ eval_id: Annotated[Optional[str], typer.Option("--eval-id", help="Filter by eval cycle ID.")] = None,
134
+ pack_hash: Annotated[Optional[str], typer.Option("--pack-hash", help="Filter by pack hash.")] = None,
135
+ limit: Annotated[int, typer.Option("--limit", "-l", help="Max results to return.")] = 50,
136
+ json_output: Annotated[bool, _json_opt] = False,
137
+ base_url: Annotated[str, _base_url_opt] = "https://trajrl.com",
138
+ ) -> None:
139
+ """Evaluation log archives uploaded by validators."""
140
+ data = _client(base_url).eval_logs(
141
+ validator=validator,
142
+ miner=miner_key,
143
+ log_type=log_type,
144
+ eval_id=eval_id,
145
+ pack_hash=pack_hash,
146
+ limit=limit,
147
+ )
148
+ if _want_json(json_output):
149
+ _print_json(data)
150
+ else:
151
+ fmt.display_logs(data)
@@ -0,0 +1,370 @@
1
+ """Rich formatters for TTY output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ console = Console()
13
+
14
+
15
+ # -- helpers ---------------------------------------------------------------
16
+
17
+ def trunc(hotkey: str | None, n: int = 6) -> str:
18
+ """Truncate a hotkey: 5Cd6ht…sn11"""
19
+ if not hotkey:
20
+ return "—"
21
+ if len(hotkey) <= n + 4:
22
+ return hotkey
23
+ return f"{hotkey[:n]}…{hotkey[-4:]}"
24
+
25
+
26
+ def relative_time(ts: str | None) -> str:
27
+ """ISO/Postgres timestamp → '2h ago'."""
28
+ if not ts:
29
+ return "—"
30
+ try:
31
+ # handle both ISO 8601 and postgres timestamp formats
32
+ clean = ts.replace("+00", "+00:00").replace(" ", "T")
33
+ if not clean.endswith("Z") and "+" not in clean[10:]:
34
+ clean += "+00:00"
35
+ dt = datetime.fromisoformat(clean.replace("Z", "+00:00"))
36
+ delta = datetime.now(timezone.utc) - dt
37
+ secs = int(delta.total_seconds())
38
+ if secs < 0:
39
+ return "just now"
40
+ if secs < 60:
41
+ return f"{secs}s ago"
42
+ if secs < 3600:
43
+ return f"{secs // 60}m ago"
44
+ if secs < 86400:
45
+ return f"{secs // 3600}h ago"
46
+ return f"{secs // 86400}d ago"
47
+ except (ValueError, TypeError):
48
+ return str(ts)
49
+
50
+
51
+ def qual(v: bool | None) -> str:
52
+ if v is None:
53
+ return "—"
54
+ return "[green]\\u2713[/]" if v else "[red]\\u2717[/]"
55
+
56
+
57
+ def cost(v: float | None) -> str:
58
+ if v is None:
59
+ return "—"
60
+ return f"${v:.4f}"
61
+
62
+
63
+ def score_fmt(v: float | None) -> str:
64
+ if v is None:
65
+ return "—"
66
+ return f"{v:.2f}"
67
+
68
+
69
+ def size_fmt(b: int | None) -> str:
70
+ if not b:
71
+ return "—"
72
+ if b < 1024:
73
+ return f"{b}B"
74
+ if b < 1024 * 1024:
75
+ return f"{b // 1024}KB"
76
+ return f"{b / (1024 * 1024):.1f}MB"
77
+
78
+
79
+ # -- command displays ------------------------------------------------------
80
+
81
+ def display_status(validators_data: dict, submissions_data: dict) -> None:
82
+ valis = validators_data.get("validators", [])
83
+ subs = submissions_data.get("submissions", [])
84
+
85
+ now = datetime.now(timezone.utc)
86
+ active = 0
87
+ latest_eval: str | None = None
88
+ models: dict[str, int] = {}
89
+ for v in valis:
90
+ # count active (seen < 1h)
91
+ if v.get("lastSeen"):
92
+ try:
93
+ clean = v["lastSeen"].replace("+00", "+00:00").replace(" ", "T")
94
+ if not clean.endswith("Z") and "+" not in clean[10:]:
95
+ clean += "+00:00"
96
+ dt = datetime.fromisoformat(clean.replace("Z", "+00:00"))
97
+ if (now - dt).total_seconds() < 3600:
98
+ active += 1
99
+ except (ValueError, TypeError):
100
+ pass
101
+ # latest eval
102
+ le = v.get("lastEvalAt")
103
+ if le and (latest_eval is None or le > latest_eval):
104
+ latest_eval = le
105
+ # models
106
+ m = v.get("llmModel")
107
+ if m:
108
+ models[m] = models.get(m, 0) + 1
109
+
110
+ passed = sum(1 for s in subs if s.get("evalStatus") == "passed")
111
+ failed = sum(1 for s in subs if s.get("evalStatus") == "failed")
112
+
113
+ model_str = ", ".join(f"{m} ({c})" for m, c in sorted(models.items(), key=lambda x: -x[1]))
114
+
115
+ lines = [
116
+ f" Validators: {len(valis)} total, {active} active (seen <1h)",
117
+ f" LLM Models: {model_str or '—'}",
118
+ f" Latest Eval: {relative_time(latest_eval)}",
119
+ f" Submissions: {passed} passed, {failed} failed (last batch)",
120
+ ]
121
+ console.print(Panel("\n".join(lines), title="Network Status", border_style="cyan"))
122
+
123
+
124
+ def display_validators(data: dict) -> None:
125
+ valis = data.get("validators", [])
126
+ table = Table(title=f"Validators ({len(valis)})")
127
+ table.add_column("Hotkey", style="cyan")
128
+ table.add_column("UID", justify="right")
129
+ table.add_column("Version")
130
+ table.add_column("LLM Model")
131
+ table.add_column("Last Eval")
132
+ table.add_column("Last Seen")
133
+ for v in valis:
134
+ table.add_row(
135
+ trunc(v.get("hotkey")),
136
+ str(v.get("uid", "—")),
137
+ v.get("version") or "—",
138
+ v.get("llmModel") or "—",
139
+ relative_time(v.get("lastEvalAt")),
140
+ relative_time(v.get("lastSeen")),
141
+ )
142
+ console.print(table)
143
+
144
+
145
+ def display_scores(data: dict) -> None:
146
+ entries = data.get("entries", [])
147
+ vali = data.get("validator", "")
148
+ table = Table(title=f"Scores from {trunc(vali)} ({len(entries)} miners)")
149
+ table.add_column("Miner", style="cyan")
150
+ table.add_column("UID", justify="right")
151
+ table.add_column("Qual", justify="center")
152
+ table.add_column("Cost", justify="right")
153
+ table.add_column("Score", justify="right")
154
+ table.add_column("Weight", justify="right")
155
+ table.add_column("Scenarios")
156
+ table.add_column("Rejected")
157
+ for e in entries:
158
+ sc = e.get("scenarioScores") or {}
159
+ passed = sum(1 for s in sc.values() if isinstance(s, dict) and s.get("qualified"))
160
+ total = len(sc)
161
+ sc_str = f"{passed}/{total}" if total else "—"
162
+
163
+ rej = ""
164
+ if e.get("rejected"):
165
+ stage = e.get("rejectionStage") or ""
166
+ rej = f"[red]{stage}[/]"
167
+
168
+ table.add_row(
169
+ trunc(e.get("minerHotkey")),
170
+ str(e.get("uid") if e.get("uid") is not None else "—"),
171
+ qual(e.get("qualified")),
172
+ cost(e.get("costUsd")),
173
+ score_fmt(e.get("score")),
174
+ score_fmt(e.get("weight")),
175
+ sc_str,
176
+ rej or "—",
177
+ )
178
+ console.print(table)
179
+
180
+
181
+ def display_miner(data: dict) -> None:
182
+ hk = data.get("hotkey", "")
183
+ uid = data.get("uid")
184
+ rank = data.get("rank")
185
+
186
+ header_parts = [
187
+ f"Rank: {rank or '—'}",
188
+ f"Qualified: {'yes' if data.get('qualified') else 'no'}",
189
+ f"Cost: {cost(data.get('totalCostUsd'))}",
190
+ f"Score: {score_fmt(data.get('score'))}",
191
+ ]
192
+ detail_parts = [
193
+ f"Confidence: {data.get('confidence', '—')}",
194
+ f"Coverage: {data.get('coverage', '—')}",
195
+ f"Active: {'yes' if data.get('isActive') else 'no'}",
196
+ f"Banned: {'yes' if data.get('isBanned') else 'no'}",
197
+ ]
198
+ pack_parts = [
199
+ f"Pack: {trunc(data.get('packHash'), 10)}",
200
+ f"Winner: {'yes' if data.get('isWinner') else 'no'}",
201
+ f"Bootstrap: {'yes' if data.get('isBootstrap') else 'no'}",
202
+ ]
203
+
204
+ lines = [
205
+ " " + " | ".join(header_parts),
206
+ " " + " | ".join(detail_parts),
207
+ " " + " | ".join(pack_parts),
208
+ ]
209
+
210
+ # ban info
211
+ ban = data.get("banRecord")
212
+ if ban and ban.get("failedPackCount", 0) > 0:
213
+ lines.append(f" [red]Ban Record: {ban['failedPackCount']} failed packs[/]")
214
+ for fp in ban.get("failedPacks", [])[:3]:
215
+ reason = (fp.get("reason") or "")[:80]
216
+ lines.append(f" - {trunc(fp.get('pack_hash'), 10)}: {reason}…")
217
+
218
+ console.print(Panel(
219
+ "\n".join(lines),
220
+ title=f"Miner {trunc(hk)} (UID {uid or '—'})",
221
+ border_style="cyan",
222
+ ))
223
+
224
+ # scenario summary
225
+ scenarios = data.get("scenarioSummary", [])
226
+ if scenarios:
227
+ st = Table(title="Scenario Summary")
228
+ st.add_column("Name")
229
+ st.add_column("Avg Cost", justify="right")
230
+ st.add_column("Avg Score", justify="right")
231
+ st.add_column("Qualified")
232
+ st.add_column("Validators", justify="right")
233
+ for s in scenarios:
234
+ st.add_row(
235
+ s.get("name", "—"),
236
+ cost(s.get("avgCost")),
237
+ score_fmt(s.get("avgScore")),
238
+ f"{s.get('qualCount', 0)}/{s.get('validatorCount', 0)}",
239
+ str(s.get("validatorCount", "—")),
240
+ )
241
+ console.print(st)
242
+
243
+ # per-validator
244
+ validators = data.get("validators", [])
245
+ if validators:
246
+ vt = Table(title="Validator Reports")
247
+ vt.add_column("Validator", style="cyan")
248
+ vt.add_column("Qual", justify="center")
249
+ vt.add_column("Cost", justify="right")
250
+ vt.add_column("Score", justify="right")
251
+ vt.add_column("Block", justify="right")
252
+ vt.add_column("Reported")
253
+ vt.add_column("Rejected")
254
+ for v in validators:
255
+ rej = ""
256
+ if v.get("rejected"):
257
+ rej = f"[red]{v.get('rejectionStage', '')}[/]"
258
+ vt.add_row(
259
+ trunc(v.get("hotkey")),
260
+ qual(v.get("qualified")),
261
+ cost(v.get("costUsd")),
262
+ score_fmt(v.get("score")),
263
+ str(v.get("blockHeight") or "—"),
264
+ relative_time(v.get("createdAt")),
265
+ rej or "—",
266
+ )
267
+ console.print(vt)
268
+
269
+ # recent submissions
270
+ subs = data.get("recentSubmissions", [])
271
+ if subs:
272
+ st2 = Table(title="Recent Submissions")
273
+ st2.add_column("Pack Hash", style="cyan")
274
+ st2.add_column("Status")
275
+ st2.add_column("Reason")
276
+ st2.add_column("Submitted")
277
+ for s in subs:
278
+ status = s.get("evalStatus", "—")
279
+ style = "green" if status == "passed" else "red"
280
+ reason = (s.get("evalReason") or "—")[:60]
281
+ st2.add_row(
282
+ trunc(s.get("packHash"), 10),
283
+ f"[{style}]{status}[/]",
284
+ reason,
285
+ relative_time(s.get("submittedAt")),
286
+ )
287
+ console.print(st2)
288
+
289
+
290
+ def display_pack(data: dict) -> None:
291
+ ph = data.get("packHash", "")
292
+ summary = data.get("summary", {})
293
+
294
+ lines = [
295
+ f" Status: {data.get('evalStatus', '—')}",
296
+ f" Miner: {trunc(data.get('minerHotkey'))} (UID {data.get('minerUid', '—')})",
297
+ f" Qualified: {qual(summary.get('qualified'))} ({summary.get('qualifiedCount', 0)}/{summary.get('validatorCount', 0)} validators)",
298
+ f" Best Cost: {cost(summary.get('bestCost'))} | Avg Cost: {cost(summary.get('avgCost'))}",
299
+ ]
300
+ if data.get("evalReason"):
301
+ lines.append(f" [red]Reason: {data['evalReason']}[/]")
302
+
303
+ console.print(Panel("\n".join(lines), title=f"Pack {trunc(ph, 10)}", border_style="cyan"))
304
+
305
+ validators = data.get("validators", [])
306
+ if validators:
307
+ for v in validators:
308
+ vt = Table(title=f"Validator {trunc(v.get('hotkey'))}")
309
+ vt.add_column("Scenario")
310
+ vt.add_column("Cost", justify="right")
311
+ vt.add_column("Score", justify="right")
312
+ vt.add_column("Qualified", justify="center")
313
+ for sc in v.get("scenarios", []):
314
+ vt.add_row(
315
+ sc.get("name", "—"),
316
+ cost(sc.get("cost")),
317
+ score_fmt(sc.get("score")),
318
+ qual(sc.get("qualified")),
319
+ )
320
+ console.print(vt)
321
+
322
+
323
+ def display_submissions(data: dict, failed_only: bool = False) -> None:
324
+ subs = data.get("submissions", [])
325
+ if failed_only:
326
+ subs = [s for s in subs if s.get("evalStatus") == "failed"]
327
+
328
+ label = "Failed Submissions" if failed_only else f"Submissions ({len(subs)})"
329
+ table = Table(title=label)
330
+ table.add_column("Miner", style="cyan")
331
+ table.add_column("Pack Hash", style="cyan")
332
+ table.add_column("Status")
333
+ table.add_column("Reason", max_width=50)
334
+ table.add_column("Submitted")
335
+ for s in subs:
336
+ status = s.get("evalStatus", "—")
337
+ style = "green" if status == "passed" else "red"
338
+ table.add_row(
339
+ trunc(s.get("minerHotkey")),
340
+ trunc(s.get("packHash"), 10),
341
+ f"[{style}]{status}[/]",
342
+ (s.get("evalReason") or "—")[:50],
343
+ relative_time(s.get("submittedAt")),
344
+ )
345
+ console.print(table)
346
+
347
+
348
+ def display_logs(data: dict) -> None:
349
+ logs = data.get("logs", [])
350
+ table = Table(title=f"Eval Logs ({len(logs)})")
351
+ table.add_column("Eval ID")
352
+ table.add_column("Type")
353
+ table.add_column("Validator", style="cyan")
354
+ table.add_column("Miner", style="cyan")
355
+ table.add_column("Pack Hash")
356
+ table.add_column("Size", justify="right")
357
+ table.add_column("GCS URL", max_width=50)
358
+ table.add_column("Created")
359
+ for log in logs:
360
+ table.add_row(
361
+ log.get("evalId", "—"),
362
+ log.get("logType", "—"),
363
+ trunc(log.get("validatorHotkey")),
364
+ trunc(log.get("minerHotkey")),
365
+ trunc(log.get("packHash"), 10),
366
+ size_fmt(log.get("sizeBytes")),
367
+ (log.get("gcsUrl") or "—")[-50:],
368
+ relative_time(log.get("createdAt")),
369
+ )
370
+ console.print(table)
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: trajrl
3
+ Version: 0.1.0
4
+ Summary: CLI for TrajectoryRL, Bittensor subnet 11 — agent-friendly access to live validator, miner, and evaluation data.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: typer>=0.9
9
+ Requires-Dist: httpx>=0.25
10
+ Requires-Dist: rich>=13.0
11
+
12
+ # trajrl
13
+
14
+ CLI for the [TrajectoryRL subnet](https://trajrl.com) (Bittensor SN11). Query live validator, miner, and evaluation data from the terminal.
15
+
16
+ Designed for AI agents (Claude Code, Cursor) and humans alike — outputs JSON when piped, Rich tables when interactive.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install trajrl
22
+ ```
23
+
24
+ ## Commands
25
+
26
+ ```
27
+ trajrl status # Network health overview
28
+ trajrl validators # List all validators
29
+ trajrl scores <validator_hotkey> # Per-miner scores from a validator
30
+ trajrl miner <hotkey> # Miner detail + diagnostics
31
+ trajrl pack <hotkey> <pack_hash> # Pack evaluation detail
32
+ trajrl submissions [--failed] # Recent pack submissions
33
+ trajrl logs [--type cycle|miner] # Eval log archives
34
+ ```
35
+
36
+ ### Global Options
37
+
38
+ Every command accepts:
39
+
40
+ | Option | Description |
41
+ |--------|-------------|
42
+ | `--json` / `-j` | Force JSON output (auto-enabled when stdout is piped) |
43
+ | `--base-url URL` | Override API base (default: `https://trajrl.com`, env: `TRAJRL_BASE_URL`) |
44
+
45
+ ## Usage Examples
46
+
47
+ ### Quick network check
48
+
49
+ ```bash
50
+ trajrl status
51
+ ```
52
+ ```
53
+ ╭──────────────────── Network Status ────────────────────╮
54
+ │ Validators: 7 total, 7 active (seen <1h) │
55
+ │ LLM Models: zhipu/glm-5 (3), chutes/GLM-5-TEE (3) │
56
+ │ Latest Eval: 7h ago │
57
+ │ Submissions: 65 passed, 35 failed (last batch) │
58
+ ╰────────────────────────────────────────────────────────╯
59
+ ```
60
+
61
+ ### List validators
62
+
63
+ ```bash
64
+ trajrl validators
65
+ ```
66
+ ```
67
+ Hotkey UID Version LLM Model Last Eval Last Seen
68
+ 5Cd6h…sn11 29 0.2.7 chutes/zai-org/GLM-5… 7h ago 2m ago
69
+ 5EcgNd…797f 221 0.2.7 zhipu/glm-5 10h ago 6m ago
70
+ ...
71
+ ```
72
+
73
+ ### Inspect a miner
74
+
75
+ ```bash
76
+ trajrl miner 5HMgR6LnNqUAtaKRwa6bLF4Vy4KBf7TaxCLehyff9mWPhSHt
77
+ ```
78
+
79
+ Shows rank, qualification status, cost, scenario breakdown, per-validator reports, recent submissions, and ban records.
80
+
81
+ ### View failed submissions
82
+
83
+ ```bash
84
+ trajrl submissions --failed
85
+ ```
86
+
87
+ ### Filter eval logs
88
+
89
+ ```bash
90
+ trajrl logs --type cycle --limit 5
91
+ trajrl logs --validator 5Cd6h... --type miner
92
+ trajrl logs --eval-id 20260324_000340
93
+ ```
94
+
95
+ ### JSON output for agents
96
+
97
+ Pipe to any tool — JSON is automatic:
98
+
99
+ ```bash
100
+ trajrl validators | jq '.validators[].hotkey'
101
+ trajrl scores 5Cd6h... --json | python3 -c "
102
+ import sys, json
103
+ d = json.load(sys.stdin)
104
+ for e in d['entries'][:5]:
105
+ print(f\"{e['minerHotkey'][:12]} qual={e['qualified']} cost={e['costUsd']}\")
106
+ "
107
+ ```
108
+
109
+ Force JSON in an interactive terminal:
110
+
111
+ ```bash
112
+ trajrl miner 5HMgR6... --json
113
+ ```
114
+
115
+ ## API Reference
116
+
117
+ All data comes from the [TrajectoryRL Public API](https://trajrl.com) — read-only, no authentication required.
118
+
119
+ | Endpoint | CLI Command |
120
+ |----------|-------------|
121
+ | `GET /api/validators` | `trajrl validators` |
122
+ | `GET /api/scores/by-validator?validator=` | `trajrl scores <hotkey>` |
123
+ | `GET /api/miners/:hotkey` | `trajrl miner <hotkey>` |
124
+ | `GET /api/miners/:hotkey/packs/:hash` | `trajrl pack <hotkey> <hash>` |
125
+ | `GET /api/submissions` | `trajrl submissions` |
126
+ | `GET /api/eval-logs` | `trajrl logs` |
127
+
128
+ ## Development
129
+
130
+ ```bash
131
+ git clone <repo> && cd trajrl
132
+ pip install -e .
133
+ trajrl --help
134
+ ```
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ trajrl/__init__.py
4
+ trajrl/api.py
5
+ trajrl/cli.py
6
+ trajrl/display.py
7
+ trajrl.egg-info/PKG-INFO
8
+ trajrl.egg-info/SOURCES.txt
9
+ trajrl.egg-info/dependency_links.txt
10
+ trajrl.egg-info/entry_points.txt
11
+ trajrl.egg-info/requires.txt
12
+ trajrl.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ trajrl = trajrl.cli:app
@@ -0,0 +1,3 @@
1
+ typer>=0.9
2
+ httpx>=0.25
3
+ rich>=13.0
@@ -0,0 +1 @@
1
+ trajrl