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 +134 -0
- trajrl-0.1.0/README.md +123 -0
- trajrl-0.1.0/pyproject.toml +19 -0
- trajrl-0.1.0/setup.cfg +4 -0
- trajrl-0.1.0/trajrl/__init__.py +1 -0
- trajrl-0.1.0/trajrl/api.py +85 -0
- trajrl-0.1.0/trajrl/cli.py +151 -0
- trajrl-0.1.0/trajrl/display.py +370 -0
- trajrl-0.1.0/trajrl.egg-info/PKG-INFO +134 -0
- trajrl-0.1.0/trajrl.egg-info/SOURCES.txt +12 -0
- trajrl-0.1.0/trajrl.egg-info/dependency_links.txt +1 -0
- trajrl-0.1.0/trajrl.egg-info/entry_points.txt +2 -0
- trajrl-0.1.0/trajrl.egg-info/requires.txt +3 -0
- trajrl-0.1.0/trajrl.egg-info/top_level.txt +1 -0
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 @@
|
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trajrl
|