nepher-cli 0.2.6__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. nepher_cli-0.2.8/PKG-INFO +177 -0
  2. nepher_cli-0.2.8/README.md +149 -0
  3. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/pyproject.toml +1 -1
  4. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/envhub.py +106 -52
  5. nepher_cli-0.2.8/src/nepher_cli/envhub/bundle.py +73 -0
  6. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/envhub/cache.py +8 -3
  7. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/envhub/config.py +42 -19
  8. nepher_cli-0.2.6/PKG-INFO +0 -200
  9. nepher_cli-0.2.6/README.md +0 -172
  10. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/.gitignore +0 -0
  11. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/__init__.py +0 -0
  12. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/__main__.py +0 -0
  13. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/cli.py +0 -0
  14. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/__init__.py +0 -0
  15. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/account.py +0 -0
  16. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/hackathon.py +0 -0
  17. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/highlight.py +0 -0
  18. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/simstore.py +0 -0
  19. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/tournament.py +0 -0
  20. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/config.py +0 -0
  21. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/core/__init__.py +0 -0
  22. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/core/credentials.py +0 -0
  23. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/core/http.py +0 -0
  24. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/envhub/__init__.py +0 -0
  25. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/py.typed +0 -0
  26. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/tournament/__init__.py +0 -0
  27. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/tournament/agent_check.py +0 -0
  28. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/tournament/api.py +0 -0
  29. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/tournament/packer.py +0 -0
  30. {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/tournament/wallet.py +0 -0
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: nepher-cli
3
+ Version: 0.2.8
4
+ Summary: npcli — unified Nepher command-line interface for accounts, hackathons, EnvHub, tournaments, and SimStore
5
+ Project-URL: Homepage, https://github.com/nepher-ai/nepher-cli
6
+ Author: Nepher
7
+ License: MIT
8
+ Keywords: bittensor,cli,envhub,hackathon,nepher,subnet,tournament
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: click>=8.1.0
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: keyring>=25.0.0
20
+ Requires-Dist: platformdirs>=4.0.0
21
+ Requires-Dist: rich>=13.7.0
22
+ Provides-Extra: bittensor
23
+ Requires-Dist: bittensor-wallet>=4.1.0; extra == 'bittensor'
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ <div align="center">
30
+
31
+ # nepher-cli
32
+
33
+ **The unified CLI for [Nepher Robotics](https://nepher.ai): accounts, tournaments, hackathons, EnvHub and SimStore.**
34
+
35
+ [![PyPI](https://img.shields.io/pypi/v/nepher-cli)](https://pypi.org/project/nepher-cli/)
36
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](#license)
38
+
39
+ [Install](#install) •
40
+ [Commands](#commands) •
41
+ [Tournaments](#tournaments) •
42
+ [EnvHub](#envhub) •
43
+ [Hackathons](#hackathons) •
44
+ [Account](#account) •
45
+ [Troubleshooting](#troubleshooting)
46
+
47
+ </div>
48
+
49
+ ---
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install nepher-cli
55
+ pip install "nepher-cli[bittensor]" # adds wallet signing for tournament submit and coldkey registration
56
+ ```
57
+
58
+ This installs two equivalent commands, `npcli` and `nepher-cli`.
59
+
60
+ ## Quick start
61
+
62
+ ```bash
63
+ npcli login --api-key nepher_xxxxxxxx
64
+ npcli whoami
65
+ npcli --help
66
+ ```
67
+
68
+ Get your API key at [nepher.ai/account](https://nepher.ai/account) under **API Keys**. In CI/CD, set `NEPHER_API_KEY` instead of logging in.
69
+
70
+ ## Commands
71
+
72
+ | Command | Description |
73
+ |---|---|
74
+ | `npcli login` / `whoami` / `logout` | Manage authentication |
75
+ | `npcli account` | API keys and coldkey registration |
76
+ | `npcli tournament` | Browse tournaments, check and submit agents, view leaderboards |
77
+ | `npcli envhub` | Manage Isaac Lab environment bundles |
78
+ | `npcli hackathon` | Browse and submit to hackathons |
79
+ | `npcli simstore` | SimStore marketplace (coming soon) |
80
+
81
+ Run any command with `--help` for all options.
82
+
83
+ ## Tournaments
84
+
85
+ ```bash
86
+ npcli tournament list # or list-active
87
+ npcli tournament status <tournament_id>
88
+ npcli tournament leaderboard <tournament_id>
89
+ npcli tournament check --path ./my-agent
90
+ npcli tournament submit --path ./my-agent --wallet-name miner --wallet-hotkey default
91
+ ```
92
+
93
+ `check` validates the agent directory (add `--verbose` for recommended-file warnings). `submit` requires `bittensor-wallet`.
94
+
95
+ <details>
96
+ <summary>Full <code>tournament submit</code> options</summary>
97
+
98
+ | Option | Description |
99
+ |---|---|
100
+ | `--path <path>` | Agent directory |
101
+ | `--wallet-name <str>` | Bittensor wallet name (default: `miner`) |
102
+ | `--wallet-hotkey <str>` | Bittensor wallet hotkey (default: `default`) |
103
+ | `--api-key <key>` | Nepher API key (falls back to stored credentials) |
104
+ | `--tournament-id <id>` | Target tournament, required when several are active |
105
+ | `--api-url <url>` | Override the tournament API URL |
106
+ | `-v`, `--verbose` | Verbose output |
107
+
108
+ </details>
109
+
110
+ ## EnvHub
111
+
112
+ ```bash
113
+ npcli envhub list
114
+ npcli envhub download <env_id>
115
+ npcli envhub upload ./my-bundle --category navigation
116
+ npcli envhub cache list | info | clear
117
+ ```
118
+
119
+ Shares login, API and cache with the standalone [`nepher`](https://github.com/nepher-ai/envhub) package.
120
+
121
+ ## Hackathons
122
+
123
+ ```bash
124
+ npcli hackathon list
125
+ npcli hackathon submit --title "My entry" --submission ./my-project --assets ./my-assets
126
+ ```
127
+
128
+ <details>
129
+ <summary>Full <code>hackathon submit</code> options</summary>
130
+
131
+ | Option | Description |
132
+ |---|---|
133
+ | `--hackathon-id <uuid>` | Required when several hackathons are open |
134
+ | `--title <str>` | Entry title |
135
+ | `--description <markdown>` | Entry description |
136
+ | `--thumbnail <file>` | Cover image (JPEG, PNG, WebP, GIF) |
137
+ | `--submission <path>` | Project folder or `submission.zip` |
138
+ | `--assets <path>` | Assets folder or `assets.zip` (images, videos, PDFs) |
139
+ | `--public-source` | Mark source as public |
140
+
141
+ </details>
142
+
143
+ ## Account
144
+
145
+ API key management needs a session from `npcli account login`. Don't pass `--api-key` to the `api-keys` commands; the account API rejects `X-API-Key` for key management.
146
+
147
+ ```bash
148
+ npcli account login --api-key nepher_...
149
+ npcli account api-keys list
150
+ npcli account api-keys create --name "CI key" --platform hackathon
151
+ npcli account api-keys revoke <key_id>
152
+ ```
153
+
154
+ Bind a Bittensor coldkey to your account (requires `bittensor-wallet`; `btcli` is not needed):
155
+
156
+ ```bash
157
+ npcli account register-coldkey --wallet <wallet_name>
158
+ ```
159
+
160
+ Keys must start with `nepher_` and be active. `hackathon submit` needs Hackathon scope and `tournament submit` needs Tournament scope, or use an unrestricted key.
161
+
162
+ ## Troubleshooting
163
+
164
+ | Error | Fix |
165
+ |---|---|
166
+ | `npcli: command not found` | Check the install succeeded and Python's scripts directory is on `PATH` |
167
+ | `Not logged in` | `npcli login --api-key nepher_...` |
168
+ | `invalid api key format` | Key must start with `nepher_` |
169
+ | `api key expired` | Create a new key with `npcli account api-keys create` |
170
+ | `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
171
+ | `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
172
+ | `bittensor-wallet not installed` | `pip install bittensor-wallet` |
173
+ | `Conflict detected: 'scalecodec' ... 'cyscale'` | Leftover `scalecodec` from a Bittensor upgrade. Install `bittensor-wallet` so npcli doesn't need `btcli`, or repair `btcli` with `pip uninstall scalecodec cyscale -y && pip install -U cyscale --force-reinstall` |
174
+
175
+ ## License
176
+
177
+ MIT © 2026 Nepher Robotics
@@ -0,0 +1,149 @@
1
+ <div align="center">
2
+
3
+ # nepher-cli
4
+
5
+ **The unified CLI for [Nepher Robotics](https://nepher.ai): accounts, tournaments, hackathons, EnvHub and SimStore.**
6
+
7
+ [![PyPI](https://img.shields.io/pypi/v/nepher-cli)](https://pypi.org/project/nepher-cli/)
8
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](#license)
10
+
11
+ [Install](#install) •
12
+ [Commands](#commands) •
13
+ [Tournaments](#tournaments) •
14
+ [EnvHub](#envhub) •
15
+ [Hackathons](#hackathons) •
16
+ [Account](#account) •
17
+ [Troubleshooting](#troubleshooting)
18
+
19
+ </div>
20
+
21
+ ---
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install nepher-cli
27
+ pip install "nepher-cli[bittensor]" # adds wallet signing for tournament submit and coldkey registration
28
+ ```
29
+
30
+ This installs two equivalent commands, `npcli` and `nepher-cli`.
31
+
32
+ ## Quick start
33
+
34
+ ```bash
35
+ npcli login --api-key nepher_xxxxxxxx
36
+ npcli whoami
37
+ npcli --help
38
+ ```
39
+
40
+ Get your API key at [nepher.ai/account](https://nepher.ai/account) under **API Keys**. In CI/CD, set `NEPHER_API_KEY` instead of logging in.
41
+
42
+ ## Commands
43
+
44
+ | Command | Description |
45
+ |---|---|
46
+ | `npcli login` / `whoami` / `logout` | Manage authentication |
47
+ | `npcli account` | API keys and coldkey registration |
48
+ | `npcli tournament` | Browse tournaments, check and submit agents, view leaderboards |
49
+ | `npcli envhub` | Manage Isaac Lab environment bundles |
50
+ | `npcli hackathon` | Browse and submit to hackathons |
51
+ | `npcli simstore` | SimStore marketplace (coming soon) |
52
+
53
+ Run any command with `--help` for all options.
54
+
55
+ ## Tournaments
56
+
57
+ ```bash
58
+ npcli tournament list # or list-active
59
+ npcli tournament status <tournament_id>
60
+ npcli tournament leaderboard <tournament_id>
61
+ npcli tournament check --path ./my-agent
62
+ npcli tournament submit --path ./my-agent --wallet-name miner --wallet-hotkey default
63
+ ```
64
+
65
+ `check` validates the agent directory (add `--verbose` for recommended-file warnings). `submit` requires `bittensor-wallet`.
66
+
67
+ <details>
68
+ <summary>Full <code>tournament submit</code> options</summary>
69
+
70
+ | Option | Description |
71
+ |---|---|
72
+ | `--path <path>` | Agent directory |
73
+ | `--wallet-name <str>` | Bittensor wallet name (default: `miner`) |
74
+ | `--wallet-hotkey <str>` | Bittensor wallet hotkey (default: `default`) |
75
+ | `--api-key <key>` | Nepher API key (falls back to stored credentials) |
76
+ | `--tournament-id <id>` | Target tournament, required when several are active |
77
+ | `--api-url <url>` | Override the tournament API URL |
78
+ | `-v`, `--verbose` | Verbose output |
79
+
80
+ </details>
81
+
82
+ ## EnvHub
83
+
84
+ ```bash
85
+ npcli envhub list
86
+ npcli envhub download <env_id>
87
+ npcli envhub upload ./my-bundle --category navigation
88
+ npcli envhub cache list | info | clear
89
+ ```
90
+
91
+ Shares login, API and cache with the standalone [`nepher`](https://github.com/nepher-ai/envhub) package.
92
+
93
+ ## Hackathons
94
+
95
+ ```bash
96
+ npcli hackathon list
97
+ npcli hackathon submit --title "My entry" --submission ./my-project --assets ./my-assets
98
+ ```
99
+
100
+ <details>
101
+ <summary>Full <code>hackathon submit</code> options</summary>
102
+
103
+ | Option | Description |
104
+ |---|---|
105
+ | `--hackathon-id <uuid>` | Required when several hackathons are open |
106
+ | `--title <str>` | Entry title |
107
+ | `--description <markdown>` | Entry description |
108
+ | `--thumbnail <file>` | Cover image (JPEG, PNG, WebP, GIF) |
109
+ | `--submission <path>` | Project folder or `submission.zip` |
110
+ | `--assets <path>` | Assets folder or `assets.zip` (images, videos, PDFs) |
111
+ | `--public-source` | Mark source as public |
112
+
113
+ </details>
114
+
115
+ ## Account
116
+
117
+ API key management needs a session from `npcli account login`. Don't pass `--api-key` to the `api-keys` commands; the account API rejects `X-API-Key` for key management.
118
+
119
+ ```bash
120
+ npcli account login --api-key nepher_...
121
+ npcli account api-keys list
122
+ npcli account api-keys create --name "CI key" --platform hackathon
123
+ npcli account api-keys revoke <key_id>
124
+ ```
125
+
126
+ Bind a Bittensor coldkey to your account (requires `bittensor-wallet`; `btcli` is not needed):
127
+
128
+ ```bash
129
+ npcli account register-coldkey --wallet <wallet_name>
130
+ ```
131
+
132
+ Keys must start with `nepher_` and be active. `hackathon submit` needs Hackathon scope and `tournament submit` needs Tournament scope, or use an unrestricted key.
133
+
134
+ ## Troubleshooting
135
+
136
+ | Error | Fix |
137
+ |---|---|
138
+ | `npcli: command not found` | Check the install succeeded and Python's scripts directory is on `PATH` |
139
+ | `Not logged in` | `npcli login --api-key nepher_...` |
140
+ | `invalid api key format` | Key must start with `nepher_` |
141
+ | `api key expired` | Create a new key with `npcli account api-keys create` |
142
+ | `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
143
+ | `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
144
+ | `bittensor-wallet not installed` | `pip install bittensor-wallet` |
145
+ | `Conflict detected: 'scalecodec' ... 'cyscale'` | Leftover `scalecodec` from a Bittensor upgrade. Install `bittensor-wallet` so npcli doesn't need `btcli`, or repair `btcli` with `pip uninstall scalecodec cyscale -y && pip install -U cyscale --force-reinstall` |
146
+
147
+ ## License
148
+
149
+ MIT © 2026 Nepher Robotics
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nepher-cli"
7
- version = "0.2.6"
7
+ version = "0.2.8"
8
8
  description = "npcli — unified Nepher command-line interface for accounts, hackathons, EnvHub, tournaments, and SimStore"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -7,7 +7,9 @@ same operations from inside the unified npcli interface.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import contextlib
10
11
  import json
12
+ import os
11
13
  import shutil
12
14
  import tempfile
13
15
  import zipfile
@@ -21,18 +23,39 @@ from rich.console import Console
21
23
  from nepher_cli.config import ENVHUB_BACKEND
22
24
  from nepher_cli.core.credentials import get_auth_headers, get_stored_api_key
23
25
  from nepher_cli.core.http import parse_error_body
26
+ from nepher_cli.envhub.bundle import extract_bundle, iter_bundle_files, safe_env_id
24
27
  from nepher_cli.envhub.cache import is_cached_env, list_cached_env_dirs, resolve_cache_dir
28
+ from nepher_cli.envhub.config import (
29
+ get_api_url as get_envhub_api_url,
30
+ )
25
31
  from nepher_cli.envhub.config import (
26
32
  get_value as get_envhub_config_value,
33
+ )
34
+ from nepher_cli.envhub.config import (
27
35
  list_values as list_envhub_config_values,
36
+ )
37
+ from nepher_cli.envhub.config import (
28
38
  mask_secret as mask_envhub_config_secret,
39
+ )
40
+ from nepher_cli.envhub.config import (
29
41
  parse_config_value,
42
+ )
43
+ from nepher_cli.envhub.config import (
30
44
  reset_config as reset_envhub_config,
45
+ )
46
+ from nepher_cli.envhub.config import (
31
47
  set_value as set_envhub_config_value,
32
48
  )
33
49
 
34
50
  console = Console(stderr=True)
35
51
 
52
+ # A numeric write timeout switches the SSL socket into timeout mode. On Windows
53
+ # that makes OpenSSL raise SSLWantWriteError as soon as the TCP window fills,
54
+ # which requests/httpcore surface as a failed upload ("Max retries exceeded"
55
+ # / SSLWantWriteError) instead of waiting for the server to read. Leave the
56
+ # write unbounded; bound connect and the wait for the upload response.
57
+ _UPLOAD_TIMEOUT = httpx.Timeout(connect=30.0, read=3600.0, write=None, pool=30.0)
58
+
36
59
 
37
60
  def _headers(api_key: str | None) -> dict[str, str]:
38
61
  h = get_auth_headers(api_key)
@@ -44,7 +67,16 @@ def _headers(api_key: str | None) -> dict[str, str]:
44
67
 
45
68
 
46
69
  def _base() -> str:
47
- return ENVHUB_BACKEND.rstrip("/")
70
+ """EnvHub API base, shared with the ``nepher`` CLI's ``api_url`` setting."""
71
+ return get_envhub_api_url() or ENVHUB_BACKEND.rstrip("/")
72
+
73
+
74
+ def _env_id_or_exit(env_id: str) -> str:
75
+ try:
76
+ return safe_env_id(env_id)
77
+ except ValueError as e:
78
+ console.print(f"[red]{e}[/red]")
79
+ raise SystemExit(1) from e
48
80
 
49
81
 
50
82
  @click.group("envhub")
@@ -146,6 +178,7 @@ def envhub_list(
146
178
  @envhub.command("download")
147
179
  @click.argument("env_id")
148
180
  @click.option("--cache-dir", type=click.Path(), default=None, help="Override local cache directory.")
181
+ @click.option("--category", default=None, help="Use the category-specific cache directory from config.")
149
182
  @click.option("--force", is_flag=True, help="Re-download even if already cached.")
150
183
  @click.option(
151
184
  "--api-key",
@@ -155,7 +188,9 @@ def envhub_list(
155
188
  help="API key for authentication. Sent directly as X-API-Key so the "
156
189
  "download succeeds regardless of any cached access-token expiry.",
157
190
  )
158
- def envhub_download(env_id: str, cache_dir: str | None, force: bool, api_key: str | None) -> None:
191
+ def envhub_download(
192
+ env_id: str, cache_dir: str | None, category: str | None, force: bool, api_key: str | None
193
+ ) -> None:
159
194
  """Download an environment bundle and cache it locally.
160
195
 
161
196
  The bundle is extracted to ~/.nepher/cache/<env_id>/ by default.
@@ -164,7 +199,8 @@ def envhub_download(env_id: str, cache_dir: str | None, force: bool, api_key: st
164
199
  is sent directly to EnvHub, which verifies it against account-backend — the
165
200
  download therefore works even if a previously cached JWT has expired.
166
201
  """
167
- cache_root = resolve_cache_dir(cache_dir)
202
+ env_id = _env_id_or_exit(env_id)
203
+ cache_root = resolve_cache_dir(cache_dir, category=category)
168
204
  env_cache = cache_root / env_id
169
205
 
170
206
  if is_cached_env(env_cache) and not force:
@@ -174,31 +210,33 @@ def envhub_download(env_id: str, cache_dir: str | None, force: bool, api_key: st
174
210
  headers = _headers(api_key)
175
211
  url = f"{_base()}/api/v1/envs/{env_id}/download"
176
212
  console.print(f"Downloading [bold]{env_id}[/bold]...")
213
+ tmp_fd, tmp_name = tempfile.mkstemp(suffix=".zip")
214
+ os.close(tmp_fd)
215
+ tmp_path = Path(tmp_name)
177
216
  try:
178
- with httpx.stream("GET", url, headers=headers, timeout=600.0, follow_redirects=True) as r:
179
- if r.status_code != 200:
180
- body = r.read().decode(errors="replace")
181
- console.print(f"[red]{parse_error_body(body) or body.strip() or f'HTTP {r.status_code}'}[/red]")
182
- raise SystemExit(1)
183
-
184
- with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
185
- tmp_path = Path(tmp.name)
186
- for chunk in r.iter_bytes(chunk_size=65536):
187
- tmp.write(chunk)
188
- except httpx.RequestError as e:
189
- console.print(f"[red]Network error[/red]: {e}")
190
- raise SystemExit(1) from e
191
-
192
- console.print("Extracting bundle...")
193
- env_cache.mkdir(parents=True, exist_ok=True)
194
- try:
195
- with zipfile.ZipFile(tmp_path, "r") as zf:
196
- zf.extractall(env_cache)
197
- except zipfile.BadZipFile:
198
- console.print("[red]Downloaded file is not a valid ZIP archive.[/red]")
199
- shutil.rmtree(env_cache, ignore_errors=True)
200
- tmp_path.unlink(missing_ok=True)
201
- raise SystemExit(1)
217
+ try:
218
+ with httpx.stream("GET", url, headers=headers, timeout=600.0, follow_redirects=True) as r:
219
+ if r.status_code != 200:
220
+ body = r.read().decode(errors="replace")
221
+ console.print(f"[red]{parse_error_body(body) or body.strip() or f'HTTP {r.status_code}'}[/red]")
222
+ raise SystemExit(1)
223
+
224
+ with open(tmp_path, "wb") as tmp:
225
+ for chunk in r.iter_bytes(chunk_size=1024 * 1024):
226
+ tmp.write(chunk)
227
+ except httpx.HTTPError as e:
228
+ console.print(f"[red]Network error[/red]: {e}")
229
+ raise SystemExit(1) from e
230
+
231
+ console.print("Extracting bundle...")
232
+ try:
233
+ extract_bundle(tmp_path, env_cache)
234
+ except zipfile.BadZipFile as e:
235
+ console.print("[red]Downloaded file is not a valid ZIP archive.[/red]")
236
+ raise SystemExit(1) from e
237
+ except (ValueError, OSError) as e:
238
+ console.print(f"[red]Invalid bundle[/red]: {e}")
239
+ raise SystemExit(1) from e
202
240
  finally:
203
241
  tmp_path.unlink(missing_ok=True)
204
242
 
@@ -239,13 +277,11 @@ def envhub_upload(path: str, category: str, benchmark: bool, force: bool, thumbn
239
277
  raise SystemExit(1)
240
278
  console.print("Zipping bundle...")
241
279
  tmp_fd, tmp_name = tempfile.mkstemp(suffix=".zip")
242
- import os
243
280
  os.close(tmp_fd)
244
281
  tmp_zip = Path(tmp_name)
245
282
  with zipfile.ZipFile(tmp_zip, "w", zipfile.ZIP_DEFLATED) as zf:
246
- for file in bundle_path.rglob("*"):
247
- if file.is_file():
248
- zf.write(file, file.relative_to(bundle_path))
283
+ for file, arcname in iter_bundle_files(bundle_path):
284
+ zf.write(file, arcname.as_posix())
249
285
  upload_path = tmp_zip
250
286
  else:
251
287
  upload_path = bundle_path
@@ -257,15 +293,16 @@ def envhub_upload(path: str, category: str, benchmark: bool, force: bool, thumbn
257
293
  if force:
258
294
  data_fields["force"] = "true"
259
295
 
260
- with open(upload_path, "rb") as f:
296
+ data_fields["duplicate_policy"] = "reject"
297
+ with contextlib.ExitStack() as stack:
298
+ f = stack.enter_context(open(upload_path, "rb"))
261
299
  files: dict[str, Any] = {"bundle": (upload_path.name, f, "application/zip")}
262
300
  if thumbnail:
263
301
  thumb_path = Path(thumbnail)
264
302
  suffix = thumb_path.suffix.lower()
265
303
  content_type = "image/png" if suffix == ".png" else ("image/webp" if suffix == ".webp" else "image/jpeg")
266
- files["thumbnail"] = (thumb_path.name, open(thumbnail, "rb"), content_type)
267
-
268
- data_fields["duplicate_policy"] = "reject"
304
+ thumb_file = stack.enter_context(open(thumb_path, "rb"))
305
+ files["thumbnail"] = (thumb_path.name, thumb_file, content_type)
269
306
 
270
307
  try:
271
308
  r = httpx.post(
@@ -273,9 +310,9 @@ def envhub_upload(path: str, category: str, benchmark: bool, force: bool, thumbn
273
310
  headers=headers,
274
311
  data=data_fields,
275
312
  files=files,
276
- timeout=600.0,
313
+ timeout=_UPLOAD_TIMEOUT,
277
314
  )
278
- except httpx.RequestError as e:
315
+ except httpx.HTTPError as e:
279
316
  console.print(f"[red]Network error[/red]: {e}")
280
317
  raise SystemExit(1) from e
281
318
 
@@ -325,15 +362,15 @@ def cache_clear(env_id: str | None, cache_dir: str | None) -> None:
325
362
  """Clear cache — all environments or a specific one."""
326
363
  root = resolve_cache_dir(cache_dir)
327
364
  if env_id:
328
- target = root / env_id
365
+ target = root / _env_id_or_exit(env_id)
329
366
  if target.exists():
330
367
  shutil.rmtree(target)
331
368
  console.print(f"[green]Cleared cache for[/green] {env_id}")
332
369
  else:
333
370
  console.print(f"[yellow]{env_id} is not cached.[/yellow]")
334
371
  else:
335
- if root.exists():
336
- shutil.rmtree(root)
372
+ for entry in list_cached_env_dirs(root):
373
+ shutil.rmtree(entry)
337
374
  console.print("[green]Cleared all cached environments.[/green]")
338
375
 
339
376
 
@@ -363,14 +400,20 @@ def cache_info(cache_dir: str | None) -> None:
363
400
  @click.argument("new_path", type=click.Path())
364
401
  @click.option("--cache-dir", type=click.Path(), default=None)
365
402
  def cache_migrate(new_path: str, cache_dir: str | None) -> None:
366
- """Move the local cache to a new directory."""
403
+ """Move cached environments to NEW_PATH and make it the configured cache_dir."""
367
404
  old_root = resolve_cache_dir(cache_dir)
368
- new_root = Path(new_path)
369
- if not old_root.exists():
370
- console.print("[yellow]Nothing to migrate — cache is empty.[/yellow]")
405
+ new_root = Path(new_path).expanduser().resolve()
406
+ if new_root == old_root:
407
+ console.print("[dim]Cache is already at that location.[/dim]")
371
408
  return
372
- new_root.parent.mkdir(parents=True, exist_ok=True)
373
- shutil.move(str(old_root), str(new_root))
409
+ new_root.mkdir(parents=True, exist_ok=True)
410
+ for entry in list_cached_env_dirs(old_root):
411
+ target = new_root / entry.name
412
+ if target.exists():
413
+ console.print(f"[red]{target} already exists; aborting.[/red]")
414
+ raise SystemExit(1)
415
+ shutil.move(str(entry), str(target))
416
+ set_envhub_config_value("cache_dir", str(new_root))
374
417
  console.print(f"[green]Cache migrated to[/green] {new_root}")
375
418
 
376
419
 
@@ -402,8 +445,8 @@ def envhub_view(env_id: str, category: str | None, scene: str | None, cache_dir:
402
445
  )
403
446
  raise SystemExit(1)
404
447
 
405
- root = resolve_cache_dir(cache_dir)
406
- env_path = root / env_id
448
+ root = resolve_cache_dir(cache_dir, category=category)
449
+ env_path = root / _env_id_or_exit(env_id)
407
450
  if not is_cached_env(env_path):
408
451
  console.print(
409
452
  f"[yellow]{env_id} is not cached.[/yellow] "
@@ -412,7 +455,7 @@ def envhub_view(env_id: str, category: str | None, scene: str | None, cache_dir:
412
455
  raise SystemExit(1)
413
456
 
414
457
  try:
415
- env = load_env(env_id, category)
458
+ env = load_env(env_id, category, cache_dir=root) if cache_dir else load_env(env_id, category)
416
459
  except Exception as e:
417
460
  console.print(f"[red]Failed to load environment[/red]: {e}")
418
461
  raise SystemExit(1) from e
@@ -428,8 +471,15 @@ def envhub_view(env_id: str, category: str | None, scene: str | None, cache_dir:
428
471
  console.print(f"Launching scene [bold]{scene}[/bold] in Isaac Sim...")
429
472
  # Actual rendering requires the full IsaacLab runtime; delegate back to the
430
473
  # installed nepher package's view script which handles AppLauncher setup.
431
- import subprocess, sys
432
- subprocess.run([sys.executable, "-m", "nepher", "view", env_id, "--scene", scene], check=False)
474
+ import subprocess
475
+ import sys
476
+
477
+ cmd = [sys.executable, "-m", "nepher", "view", env_id, "--scene", scene]
478
+ if category:
479
+ cmd += ["--category", category]
480
+ if cache_dir:
481
+ cmd += ["--cache-dir", str(root)]
482
+ raise SystemExit(subprocess.run(cmd, check=False).returncode)
433
483
 
434
484
 
435
485
  # ---------------------------------------------------------------------------
@@ -459,7 +509,11 @@ def config_get(key: str) -> None:
459
509
  def config_set(key: str, value: str) -> None:
460
510
  """Set a configuration value."""
461
511
  parsed = parse_config_value(value)
462
- set_envhub_config_value(key, parsed)
512
+ try:
513
+ set_envhub_config_value(key, parsed)
514
+ except ValueError as e:
515
+ console.print(f"[red]{e}[/red]")
516
+ raise SystemExit(1) from e
463
517
  display = mask_envhub_config_secret(key, parsed) if isinstance(parsed, str) else parsed
464
518
  console.print(f"[green]Set[/green] {key} = {display}")
465
519
 
@@ -0,0 +1,73 @@
1
+ """EnvHub bundle helpers — safe extraction and zipping (same rules as ``nepher``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shutil
7
+ import tempfile
8
+ import zipfile
9
+ from collections.abc import Iterator
10
+ from pathlib import Path, PurePosixPath, PureWindowsPath
11
+
12
+ _ENV_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
13
+
14
+ EXCLUDED_NAMES = {".git", ".hg", ".svn", "__pycache__", ".DS_Store", ".env", ".venv", "venv"}
15
+ EXCLUDED_SUFFIXES = {".pyc", ".pyo"}
16
+
17
+
18
+ def safe_env_id(env_id: str) -> str:
19
+ """Return ``env_id`` if it is a single safe path segment, else raise ValueError."""
20
+ if not _ENV_ID_RE.match(env_id) or ".." in env_id:
21
+ raise ValueError(f"Invalid environment ID: {env_id!r}")
22
+ return env_id
23
+
24
+
25
+ def _check_member(dest: Path, name: str) -> None:
26
+ win = PureWindowsPath(name)
27
+ if PurePosixPath(name).is_absolute() or win.is_absolute() or win.drive:
28
+ raise ValueError(f"Unsafe path in bundle: {name!r}")
29
+ if not (dest / name).resolve().is_relative_to(dest):
30
+ raise ValueError(f"Unsafe path in bundle: {name!r}")
31
+
32
+
33
+ def extract_bundle(zip_path: Path, dest_dir: Path) -> None:
34
+ """Extract a bundle into ``dest_dir`` atomically.
35
+
36
+ Members that would land outside the destination are rejected, the
37
+ uncompressed size is checked against free disk space, and the result only
38
+ replaces ``dest_dir`` once it contains a root ``manifest.yaml``.
39
+ """
40
+ dest_dir.parent.mkdir(parents=True, exist_ok=True)
41
+ staging = Path(tempfile.mkdtemp(prefix=f".{dest_dir.name}.", dir=dest_dir.parent))
42
+ try:
43
+ with zipfile.ZipFile(zip_path, "r") as zf:
44
+ members = zf.infolist()
45
+ root = staging.resolve()
46
+ for member in members:
47
+ _check_member(root, member.filename)
48
+ total = sum(m.file_size for m in members)
49
+ free = shutil.disk_usage(staging).free
50
+ if total > free:
51
+ need, have = total / 1024**3, free / 1024**3
52
+ raise ValueError(f"Bundle needs {need:.2f} GB but only {have:.2f} GB is free")
53
+ zf.extractall(staging, members=members)
54
+
55
+ if not (staging / "manifest.yaml").is_file():
56
+ raise ValueError("manifest.yaml not found at the root of the bundle")
57
+
58
+ if dest_dir.exists():
59
+ shutil.rmtree(dest_dir)
60
+ staging.rename(dest_dir)
61
+ finally:
62
+ if staging.exists():
63
+ shutil.rmtree(staging, ignore_errors=True)
64
+
65
+
66
+ def iter_bundle_files(bundle_dir: Path) -> Iterator[tuple[Path, Path]]:
67
+ """Yield ``(path, archive_name)`` for files to include when zipping a bundle."""
68
+ for path in sorted(bundle_dir.rglob("*")):
69
+ rel = path.relative_to(bundle_dir)
70
+ if any(part in EXCLUDED_NAMES for part in rel.parts):
71
+ continue
72
+ if path.is_file() and path.suffix not in EXCLUDED_SUFFIXES:
73
+ yield path, rel
@@ -24,11 +24,16 @@ def resolve_cache_dir(cache_dir: str | Path | None = None, category: str | None
24
24
  if cache_dir:
25
25
  return Path(cache_dir).expanduser().resolve()
26
26
 
27
+ # Read the env var directly: an already-initialised ``nepher`` config may
28
+ # predate it. Category-specific cache_dir still wins, as in ``nepher``.
29
+ env_override = os.getenv("NEPHER_CACHE_DIR")
30
+ if env_override and not category:
31
+ return Path(env_override).expanduser().resolve()
32
+
27
33
  nepher_dir = _nepher_cache_dir(category)
28
34
  if nepher_dir is not None:
29
35
  return nepher_dir
30
36
 
31
- env_override = os.getenv("NEPHER_CACHE_DIR")
32
37
  if env_override:
33
38
  return Path(env_override).expanduser().resolve()
34
39
 
@@ -50,7 +55,7 @@ def is_cached_env(path: Path) -> bool:
50
55
 
51
56
 
52
57
  def list_cached_env_dirs(root: Path) -> list[Path]:
53
- """List cached environment directories under ``root``."""
58
+ """List cached environment directories under ``root`` (skips in-progress staging dirs)."""
54
59
  if not root.exists():
55
60
  return []
56
- return sorted(p for p in root.iterdir() if is_cached_env(p))
61
+ return sorted(p for p in root.iterdir() if not p.name.startswith(".") and is_cached_env(p))
@@ -17,18 +17,32 @@ DEFAULT_CONFIG: dict[str, Any] = {
17
17
 
18
18
  LIST_KEYS = ("api_url", "cache_dir", "default_category")
19
19
 
20
+ # Older ``nepher`` releases persisted this default into config.toml; treat it as unset.
21
+ LEGACY_API_URLS = frozenset({"https://envhub-api.nepher.ai", "https://envhub-api.nepher.ai/"})
22
+
23
+ SECRET_KEYS = ("api_key",)
24
+
20
25
 
21
26
  def find_config_file() -> Path | None:
22
27
  """Return the active nepher config file path, if any."""
23
28
  cwd_config = Path.cwd() / ".nepherrc"
24
29
  if cwd_config.exists():
25
30
  return cwd_config
26
- home_config = Path.home() / ".nepher" / "config.toml"
27
- if home_config.exists():
28
- return home_config
31
+ for name in ("config.toml", "config.json"):
32
+ home_config = Path.home() / ".nepher" / name
33
+ if home_config.exists():
34
+ return home_config
29
35
  return None
30
36
 
31
37
 
38
+ def _load_toml(text: str) -> dict[str, Any]:
39
+ try:
40
+ import tomllib
41
+ except ImportError:
42
+ import tomli as tomllib # type: ignore[no-redef]
43
+ return tomllib.loads(text)
44
+
45
+
32
46
  def load_config_file() -> dict[str, Any]:
33
47
  """Load values from the nepher config file (no defaults or env overrides)."""
34
48
  config_file = find_config_file()
@@ -36,24 +50,23 @@ def load_config_file() -> dict[str, Any]:
36
50
  return {}
37
51
 
38
52
  try:
39
- if config_file.suffix == ".toml":
53
+ text = config_file.read_text(encoding="utf-8")
54
+ if config_file.suffix == ".json":
55
+ data = json.loads(text)
56
+ elif config_file.suffix == ".toml":
57
+ data = _load_toml(text)
58
+ else:
40
59
  try:
41
- import tomllib
42
- except ImportError:
43
- try:
44
- import tomli as tomllib # type: ignore[no-redef]
45
- except ImportError:
46
- return {}
47
- with open(config_file, "rb") as f:
48
- data = tomllib.load(f)
49
- return data if isinstance(data, dict) else {}
50
- if config_file.suffix == ".json" or config_file.name == ".nepherrc":
51
- with open(config_file, encoding="utf-8") as f:
52
- data = json.load(f)
53
- return data if isinstance(data, dict) else {}
60
+ data = _load_toml(text)
61
+ except Exception:
62
+ data = json.loads(text)
54
63
  except Exception:
55
64
  return {}
56
- return {}
65
+ if not isinstance(data, dict):
66
+ return {}
67
+ if data.get("api_url") in LEGACY_API_URLS:
68
+ data.pop("api_url")
69
+ return data
57
70
 
58
71
 
59
72
  def _apply_env_overrides(config: dict[str, Any]) -> dict[str, Any]:
@@ -88,9 +101,17 @@ def get_value(key: str, default: Any = None) -> Any:
88
101
  try:
89
102
  from nepher.config import get_config
90
103
 
91
- return get_config().get(key, default)
104
+ value = get_config().get(key, default)
92
105
  except ImportError:
93
106
  return _fallback_get(key, default)
107
+ if key == "api_url" and value in LEGACY_API_URLS:
108
+ return _fallback_get(key, default)
109
+ return value
110
+
111
+
112
+ def get_api_url() -> str:
113
+ """Effective EnvHub API base URL (env vars > config file > default)."""
114
+ return str(get_value("api_url") or DEFAULT_CONFIG["api_url"]).rstrip("/")
94
115
 
95
116
 
96
117
  def list_values() -> dict[str, Any]:
@@ -111,6 +132,8 @@ def parse_config_value(value: str) -> Any:
111
132
 
112
133
  def set_value(key: str, value: Any) -> None:
113
134
  """Persist a configuration value."""
135
+ if key in SECRET_KEYS:
136
+ raise ValueError("API keys are not stored in config; use 'npcli login' or NEPHER_API_KEY.")
114
137
  try:
115
138
  from nepher.config import set_config
116
139
 
nepher_cli-0.2.6/PKG-INFO DELETED
@@ -1,200 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: nepher-cli
3
- Version: 0.2.6
4
- Summary: npcli — unified Nepher command-line interface for accounts, hackathons, EnvHub, tournaments, and SimStore
5
- Project-URL: Homepage, https://github.com/nepher-ai/nepher-cli
6
- Author: Nepher
7
- License: MIT
8
- Keywords: bittensor,cli,envhub,hackathon,nepher,subnet,tournament
9
- Classifier: Development Status :: 4 - Beta
10
- Classifier: Environment :: Console
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Requires-Python: >=3.10
17
- Requires-Dist: click>=8.1.0
18
- Requires-Dist: httpx>=0.27.0
19
- Requires-Dist: keyring>=25.0.0
20
- Requires-Dist: platformdirs>=4.0.0
21
- Requires-Dist: rich>=13.7.0
22
- Provides-Extra: bittensor
23
- Requires-Dist: bittensor-wallet>=4.1.0; extra == 'bittensor'
24
- Provides-Extra: dev
25
- Requires-Dist: pytest>=8.0.0; extra == 'dev'
26
- Requires-Dist: ruff>=0.4.0; extra == 'dev'
27
- Description-Content-Type: text/markdown
28
-
29
- # nepher-cli
30
-
31
- The unified CLI for [Nepher Robotics](https://nepher.ai) — accounts, hackathons, EnvHub, tournaments, and SimStore from a single tool.
32
-
33
- **Requires:** Python 3.10+
34
-
35
- ## Install
36
-
37
- ```bash
38
- pip install nepher-cli
39
- ```
40
-
41
- Both `npcli` and `nepher-cli` entry points are registered after install.
42
-
43
- ```bash
44
- npcli --version
45
- npcli --help
46
- ```
47
-
48
- ## Commands
49
-
50
- | Command / Group | Description |
51
- |-----------------|-------------|
52
- | `npcli login` | Log in with a Nepher API key |
53
- | `npcli whoami` | Show the currently authenticated user |
54
- | `npcli logout` | Clear locally stored credentials |
55
- | `npcli account` | API keys and coldkey registration |
56
- | `npcli tournament` | Browse tournaments, submit agents, leaderboards |
57
- | `npcli envhub` | Manage Isaac Lab environment bundles |
58
- | `npcli hackathon` | Browse and submit to hackathons |
59
- | `npcli simstore` | SimStore marketplace *(coming soon)* |
60
-
61
- ## Quick Start
62
-
63
- ### Authenticate
64
-
65
- ```bash
66
- npcli login --api-key nepher_xxxxxxxx
67
- npcli whoami
68
- npcli logout
69
- ```
70
-
71
- Get your API key at [nepher.ai/account](https://nepher.ai/account) → Account → API Keys.
72
- For CI/CD, set `NEPHER_API_KEY=nepher_xxxxxxxx` instead of logging in.
73
-
74
- ### Tournaments
75
-
76
- ```bash
77
- npcli tournament list
78
- npcli tournament list-active
79
- npcli tournament status <tournament_id>
80
- npcli tournament leaderboard <tournament_id>
81
- ```
82
-
83
- Check your agent directory structure (no extra dependencies needed):
84
-
85
- ```bash
86
- npcli tournament check --path ./my-agent
87
- npcli tournament check --path ./my-agent --verbose # also show recommended-file warnings
88
- ```
89
-
90
- Submit an agent (requires `bittensor-wallet` for wallet signing):
91
-
92
- ```bash
93
- pip install bittensor-wallet
94
- # or: pip install "nepher-cli[bittensor]"
95
- ```
96
-
97
- ```bash
98
- npcli tournament submit --path ./my-agent --wallet-name miner --wallet-hotkey default
99
- ```
100
-
101
- Full options for `submit`:
102
-
103
- ```
104
- --path <path> Agent directory
105
- --wallet-name <str> Bittensor wallet name (default: miner)
106
- --wallet-hotkey <str> Bittensor wallet hotkey (default: default)
107
- --api-key <key> Nepher API key (falls back to stored credentials)
108
- --tournament-id <id> Target tournament ID (required when multiple are active)
109
- --api-url <url> Override tournament API URL
110
- -v / --verbose Verbose output
111
- ```
112
-
113
- ### EnvHub
114
-
115
- ```bash
116
- npcli envhub list
117
- npcli envhub download <env_id>
118
- npcli envhub upload ./my-bundle --category navigation
119
- npcli envhub cache list
120
- npcli envhub cache info
121
- npcli envhub cache clear
122
- ```
123
-
124
- ### Hackathons
125
-
126
- ```bash
127
- npcli hackathon list
128
-
129
- npcli hackathon submit \
130
- --title "My entry" \
131
- --submission ./my-project \
132
- --assets ./my-assets
133
- ```
134
-
135
- Full options:
136
-
137
- ```
138
- --hackathon-id <uuid> Required when multiple hackathons are open
139
- --title <str> Entry title
140
- --description <markdown> Entry description
141
- --thumbnail <file> Cover image (JPEG, PNG, WebP, GIF)
142
- --submission <path> Project folder or submission.zip
143
- --assets <path> Assets folder or assets.zip (images, videos, PDFs)
144
- --public-source Mark source as public
145
- ```
146
-
147
- ### API Keys
148
-
149
- `list` / `create` / `revoke` need a **JWT session** from `npcli account login`.
150
- Do **not** pass `--api-key` on these commands (that flag sends `X-API-Key`, which the account API rejects for key management).
151
-
152
- ```bash
153
- npcli account login --api-key nepher_...
154
- npcli account api-keys list
155
- npcli account api-keys create --name "CI key" --platform hackathon
156
- npcli account api-keys revoke <key_id>
157
- ```
158
-
159
- ### Coldkey Registration
160
-
161
- Bind a Bittensor coldkey to your account (requires `bittensor-wallet`; `btcli` is optional fallback):
162
-
163
- ```bash
164
- pip install bittensor-wallet
165
- npcli account register-coldkey --wallet <wallet_name>
166
- ```
167
-
168
- Signing uses the local coldkey via `bittensor-wallet` and does not import `btcli`. If you still have a mixed Bittensor venv (`scalecodec` + `cyscale`), `register-coldkey` keeps working as long as `bittensor-wallet` is installed.
169
-
170
- ## API Key Requirements
171
-
172
- - Must start with `nepher_`
173
- - Must be active and not expired
174
- - `hackathon submit` requires **Hackathon** scope (or an unrestricted key)
175
- - `tournament submit` requires **Tournament** scope (or an unrestricted key)
176
-
177
- ## Troubleshooting
178
-
179
- | Error | Fix |
180
- |-------|-----|
181
- | `npcli: command not found` | Ensure `pip install` succeeded and Python scripts dir is on `PATH` |
182
- | `invalid api key format` | Key must start with `nepher_` |
183
- | `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
184
- | `api key expired` | Run `npcli account api-keys create` |
185
- | `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
186
- | `bittensor-wallet not installed` | `pip install bittensor-wallet` (needed for `tournament submit` and `account register-coldkey`) |
187
- | `Not logged in` | `npcli login --api-key nepher_...` |
188
- | `Conflict detected: 'scalecodec' ... conflicts with 'cyscale'` | Leftover `scalecodec` after a Bittensor upgrade. Prefer `pip install bittensor-wallet` so npcli never needs `btcli`. To repair `btcli` itself: `pip uninstall scalecodec cyscale -y && pip install -U cyscale --force-reinstall` |
189
-
190
- Run any command with `--help` for full flag details:
191
-
192
- ```bash
193
- npcli --help
194
- npcli hackathon submit --help
195
- npcli tournament submit --help
196
- ```
197
-
198
- ## License
199
-
200
- MIT
@@ -1,172 +0,0 @@
1
- # nepher-cli
2
-
3
- The unified CLI for [Nepher Robotics](https://nepher.ai) — accounts, hackathons, EnvHub, tournaments, and SimStore from a single tool.
4
-
5
- **Requires:** Python 3.10+
6
-
7
- ## Install
8
-
9
- ```bash
10
- pip install nepher-cli
11
- ```
12
-
13
- Both `npcli` and `nepher-cli` entry points are registered after install.
14
-
15
- ```bash
16
- npcli --version
17
- npcli --help
18
- ```
19
-
20
- ## Commands
21
-
22
- | Command / Group | Description |
23
- |-----------------|-------------|
24
- | `npcli login` | Log in with a Nepher API key |
25
- | `npcli whoami` | Show the currently authenticated user |
26
- | `npcli logout` | Clear locally stored credentials |
27
- | `npcli account` | API keys and coldkey registration |
28
- | `npcli tournament` | Browse tournaments, submit agents, leaderboards |
29
- | `npcli envhub` | Manage Isaac Lab environment bundles |
30
- | `npcli hackathon` | Browse and submit to hackathons |
31
- | `npcli simstore` | SimStore marketplace *(coming soon)* |
32
-
33
- ## Quick Start
34
-
35
- ### Authenticate
36
-
37
- ```bash
38
- npcli login --api-key nepher_xxxxxxxx
39
- npcli whoami
40
- npcli logout
41
- ```
42
-
43
- Get your API key at [nepher.ai/account](https://nepher.ai/account) → Account → API Keys.
44
- For CI/CD, set `NEPHER_API_KEY=nepher_xxxxxxxx` instead of logging in.
45
-
46
- ### Tournaments
47
-
48
- ```bash
49
- npcli tournament list
50
- npcli tournament list-active
51
- npcli tournament status <tournament_id>
52
- npcli tournament leaderboard <tournament_id>
53
- ```
54
-
55
- Check your agent directory structure (no extra dependencies needed):
56
-
57
- ```bash
58
- npcli tournament check --path ./my-agent
59
- npcli tournament check --path ./my-agent --verbose # also show recommended-file warnings
60
- ```
61
-
62
- Submit an agent (requires `bittensor-wallet` for wallet signing):
63
-
64
- ```bash
65
- pip install bittensor-wallet
66
- # or: pip install "nepher-cli[bittensor]"
67
- ```
68
-
69
- ```bash
70
- npcli tournament submit --path ./my-agent --wallet-name miner --wallet-hotkey default
71
- ```
72
-
73
- Full options for `submit`:
74
-
75
- ```
76
- --path <path> Agent directory
77
- --wallet-name <str> Bittensor wallet name (default: miner)
78
- --wallet-hotkey <str> Bittensor wallet hotkey (default: default)
79
- --api-key <key> Nepher API key (falls back to stored credentials)
80
- --tournament-id <id> Target tournament ID (required when multiple are active)
81
- --api-url <url> Override tournament API URL
82
- -v / --verbose Verbose output
83
- ```
84
-
85
- ### EnvHub
86
-
87
- ```bash
88
- npcli envhub list
89
- npcli envhub download <env_id>
90
- npcli envhub upload ./my-bundle --category navigation
91
- npcli envhub cache list
92
- npcli envhub cache info
93
- npcli envhub cache clear
94
- ```
95
-
96
- ### Hackathons
97
-
98
- ```bash
99
- npcli hackathon list
100
-
101
- npcli hackathon submit \
102
- --title "My entry" \
103
- --submission ./my-project \
104
- --assets ./my-assets
105
- ```
106
-
107
- Full options:
108
-
109
- ```
110
- --hackathon-id <uuid> Required when multiple hackathons are open
111
- --title <str> Entry title
112
- --description <markdown> Entry description
113
- --thumbnail <file> Cover image (JPEG, PNG, WebP, GIF)
114
- --submission <path> Project folder or submission.zip
115
- --assets <path> Assets folder or assets.zip (images, videos, PDFs)
116
- --public-source Mark source as public
117
- ```
118
-
119
- ### API Keys
120
-
121
- `list` / `create` / `revoke` need a **JWT session** from `npcli account login`.
122
- Do **not** pass `--api-key` on these commands (that flag sends `X-API-Key`, which the account API rejects for key management).
123
-
124
- ```bash
125
- npcli account login --api-key nepher_...
126
- npcli account api-keys list
127
- npcli account api-keys create --name "CI key" --platform hackathon
128
- npcli account api-keys revoke <key_id>
129
- ```
130
-
131
- ### Coldkey Registration
132
-
133
- Bind a Bittensor coldkey to your account (requires `bittensor-wallet`; `btcli` is optional fallback):
134
-
135
- ```bash
136
- pip install bittensor-wallet
137
- npcli account register-coldkey --wallet <wallet_name>
138
- ```
139
-
140
- Signing uses the local coldkey via `bittensor-wallet` and does not import `btcli`. If you still have a mixed Bittensor venv (`scalecodec` + `cyscale`), `register-coldkey` keeps working as long as `bittensor-wallet` is installed.
141
-
142
- ## API Key Requirements
143
-
144
- - Must start with `nepher_`
145
- - Must be active and not expired
146
- - `hackathon submit` requires **Hackathon** scope (or an unrestricted key)
147
- - `tournament submit` requires **Tournament** scope (or an unrestricted key)
148
-
149
- ## Troubleshooting
150
-
151
- | Error | Fix |
152
- |-------|-----|
153
- | `npcli: command not found` | Ensure `pip install` succeeded and Python scripts dir is on `PATH` |
154
- | `invalid api key format` | Key must start with `nepher_` |
155
- | `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
156
- | `api key expired` | Run `npcli account api-keys create` |
157
- | `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
158
- | `bittensor-wallet not installed` | `pip install bittensor-wallet` (needed for `tournament submit` and `account register-coldkey`) |
159
- | `Not logged in` | `npcli login --api-key nepher_...` |
160
- | `Conflict detected: 'scalecodec' ... conflicts with 'cyscale'` | Leftover `scalecodec` after a Bittensor upgrade. Prefer `pip install bittensor-wallet` so npcli never needs `btcli`. To repair `btcli` itself: `pip uninstall scalecodec cyscale -y && pip install -U cyscale --force-reinstall` |
161
-
162
- Run any command with `--help` for full flag details:
163
-
164
- ```bash
165
- npcli --help
166
- npcli hackathon submit --help
167
- npcli tournament submit --help
168
- ```
169
-
170
- ## License
171
-
172
- MIT
File without changes