kiteml-cli 0.4.0__tar.gz → 0.5.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.
Files changed (23) hide show
  1. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/PKG-INFO +22 -15
  2. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/README.md +19 -12
  3. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/pyproject.toml +2 -2
  4. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/__init__.py +1 -1
  5. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/cli/commands/augment.py +13 -6
  6. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/cli/commands/evaluations.py +39 -15
  7. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/client.py +11 -0
  8. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/mcp/server.py +31 -18
  9. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/.gitignore +0 -0
  10. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/archive.py +0 -0
  11. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/auth.py +0 -0
  12. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/cli/__init__.py +0 -0
  13. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/cli/commands/__init__.py +0 -0
  14. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/cli/commands/auth_cmd.py +0 -0
  15. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/cli/commands/config_cmd.py +0 -0
  16. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/cli/commands/rl.py +0 -0
  17. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/cli/commands/twin.py +0 -0
  18. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/cli/main.py +0 -0
  19. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/config.py +0 -0
  20. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/mcp/__init__.py +0 -0
  21. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/mcp/common.py +0 -0
  22. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/output.py +0 -0
  23. {kiteml_cli-0.4.0 → kiteml_cli-0.5.0}/src/kite_cli/progress.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kiteml-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Kite CLI and MCP server: train robot policies with reinforcement learning in simulation, augment LeRobot datasets, and build MuJoCo digital twins, from your terminal or an AI agent.
5
5
  Project-URL: Homepage, https://kiteml.com
6
- Project-URL: Documentation, https://kite-ml.mintlify.app/tools/cli-and-mcp
7
- Project-URL: API reference, https://kite-ml.mintlify.app/platform-api/overview
6
+ Project-URL: Documentation, https://docs.kiteml.com/tools/cli-and-mcp
7
+ Project-URL: API reference, https://docs.kiteml.com/platform-api/overview
8
8
  Project-URL: Dashboard, https://app.kiteml.com
9
9
  Project-URL: MCP server, https://mcp.kiteml.com/mcp
10
10
  Project-URL: Blog, https://kiteml.com/blog
@@ -43,8 +43,8 @@ Description-Content-Type: text/markdown
43
43
  # Kite CLI and MCP server
44
44
 
45
45
  Robot learning on cloud GPUs, from your terminal or your AI agent. Kite trains robot policies with
46
- reinforcement learning in simulation, augments LeRobot datasets, and rebuilds robot episodes as MuJoCo digital
47
- twins. This package gives you the `kite` command line and the `kite-mcp` server for Claude, Cursor, and any
46
+ reinforcement learning in simulation, evaluates policies on Kite SDK specs, augments LeRobot datasets, and rebuilds
47
+ robot episodes as MuJoCo digital twins. This package gives you the `kite` command line and the `kite-mcp` server for Claude, Cursor, and any
48
48
  Model Context Protocol (MCP) client.
49
49
 
50
50
  <!-- mcp-name: com.kiteml/kite -->
@@ -52,11 +52,11 @@ Model Context Protocol (MCP) client.
52
52
  - **RL training in simulation.** Describe a behavior in plain English. Kite writes the task spec, checks its
53
53
  rewards for free, and trains the policy on a GPU. You get the policy as ONNX, the MuJoCo scene it trained in, and
54
54
  clips.
55
- - **Policy evaluation.** Every RL run ends with a report: a pass, needs_review or fail verdict from measured checks
55
+ - **RL verdicts.** Every RL run ends with a report: a pass, needs_review or fail verdict from measured checks
56
56
  (falls per minute, survival, command tracking, gait, posture), plus a vision model's read of the clip.
57
- - **Dataset augmentation.** Relight a LeRobot dataset to match a deployment scene, or generate new variations of
58
- it with video augmentation. Output is a standard LeRobot dataset (Parquet + MP4), downloaded or pushed to
59
- Hugging Face.
57
+ - **Dataset augmentation.** Relight a LeRobot dataset on Hugging Face or in a public S3 bucket to match a deployment
58
+ scene, or generate new variations of it with video augmentation. Output is a standard LeRobot dataset (Parquet +
59
+ MP4), downloaded or pushed to Hugging Face.
60
60
  - **Digital twins (beta).** Point Kite at one episode of a LeRobot dataset and get an interactable MuJoCo scene of the
61
61
  room, with the objects the robot handles built to size.
62
62
  - **Evaluations (beta).** Evaluate a policy by running a Kite SDK spec on Kite's machines, on an NVIDIA L4 for a GPU
@@ -106,7 +106,7 @@ lerobot/pusht to look like this photo".
106
106
  | --- | --- |
107
107
  | `kite_rl_catalog`, `kite_rl_plan`, `kite_rl_validate` | See which robots and objectives RL runs support, and plan and check a training spec for free |
108
108
  | `kite_rl_train`, `kite_rl_fork`, `kite_rl_cancel` | Start, branch from, and stop RL runs |
109
- | `kite_rl_status`, `kite_rl_metrics`, `kite_rl_report`, `kite_rl_list` | Follow training and read each run's evaluation verdict |
109
+ | `kite_rl_status`, `kite_rl_metrics`, `kite_rl_report`, `kite_rl_list` | Follow training and read each run's verdict |
110
110
  | `kite_augment_create`, `kite_augment_status`, `kite_augment_list`, `kite_augment_cancel` | Augment LeRobot datasets |
111
111
  | `kite_twin_validate`, `kite_twin_create`, `kite_twin_status`, `kite_twin_list`, `kite_twin_cancel`, `kite_twin_resume` | Build MuJoCo digital twins (beta) |
112
112
  | `kite_eval_validate`, `kite_eval_run`, `kite_eval_status`, `kite_eval_list`, `kite_eval_report`, `kite_eval_logs`, `kite_eval_cancel`, `kite_eval_resume` | Evaluate a policy on a Kite SDK spec on Kite's machines and read the scorecard (beta) |
@@ -132,6 +132,9 @@ kite augment create --repo-id lerobot/pusht \
132
132
  kite augment download aug_... -o ./pusht-marble
133
133
  ```
134
134
 
135
+ `--repo-id` takes a Hugging Face id (`lerobot/pusht`), its `huggingface.co/datasets/` URL, or an S3 link to the
136
+ dataset's folder in a public bucket (`s3://bucket/dataset/`).
137
+
135
138
  Build a digital twin (beta, enabled per account):
136
139
 
137
140
  ```bash
@@ -145,16 +148,20 @@ kite eval validate specs/smoke.yaml # free: the plan and what a run r
145
148
  kite eval run specs/smoke.yaml --out ./runs # -> ./runs/eval_.../scorecard.json
146
149
  ```
147
150
 
151
+ A file too big to send, such as a checkpoint's weights, can come from the Hugging Face Hub or a public S3 bucket:
152
+ `--hf PATH=OWNER/NAME@COMMIT:FILE` or `--hf PATH=<the file's huggingface.co URL>`, and `--s3 PATH=s3://BUCKET/KEY`,
153
+ PATH being the path the spec names.
154
+
148
155
  Every command prints JSON (`{"ok": true, "data": ...}`) so scripts and agents can parse it. Run `kite --help` for
149
156
  the full list.
150
157
 
151
158
  ## Links
152
159
 
153
- - Docs: [CLI and MCP](https://kite-ml.mintlify.app/tools/cli-and-mcp) ·
154
- [RL runs](https://kite-ml.mintlify.app/platform-api/rl-runs) ·
155
- [Augmentations](https://kite-ml.mintlify.app/platform-api/augmentation) ·
156
- [Twins](https://kite-ml.mintlify.app/platform-api/twins) ·
157
- [Evaluations](https://kite-ml.mintlify.app/platform-api/evaluations)
160
+ - Docs: [CLI and MCP](https://docs.kiteml.com/tools/cli-and-mcp) ·
161
+ [RL runs](https://docs.kiteml.com/platform-api/rl-runs) ·
162
+ [Augmentations](https://docs.kiteml.com/platform-api/augmentation) ·
163
+ [Twins](https://docs.kiteml.com/platform-api/twins) ·
164
+ [Evaluations](https://docs.kiteml.com/platform-api/evaluations)
158
165
  - Dashboard: [app.kiteml.com](https://app.kiteml.com)
159
166
  - Website: [kiteml.com](https://kiteml.com)
160
167
  - Support: [raul@kiteml.com](mailto:raul@kiteml.com)
@@ -1,8 +1,8 @@
1
1
  # Kite CLI and MCP server
2
2
 
3
3
  Robot learning on cloud GPUs, from your terminal or your AI agent. Kite trains robot policies with
4
- reinforcement learning in simulation, augments LeRobot datasets, and rebuilds robot episodes as MuJoCo digital
5
- twins. This package gives you the `kite` command line and the `kite-mcp` server for Claude, Cursor, and any
4
+ reinforcement learning in simulation, evaluates policies on Kite SDK specs, augments LeRobot datasets, and rebuilds
5
+ robot episodes as MuJoCo digital twins. This package gives you the `kite` command line and the `kite-mcp` server for Claude, Cursor, and any
6
6
  Model Context Protocol (MCP) client.
7
7
 
8
8
  <!-- mcp-name: com.kiteml/kite -->
@@ -10,11 +10,11 @@ Model Context Protocol (MCP) client.
10
10
  - **RL training in simulation.** Describe a behavior in plain English. Kite writes the task spec, checks its
11
11
  rewards for free, and trains the policy on a GPU. You get the policy as ONNX, the MuJoCo scene it trained in, and
12
12
  clips.
13
- - **Policy evaluation.** Every RL run ends with a report: a pass, needs_review or fail verdict from measured checks
13
+ - **RL verdicts.** Every RL run ends with a report: a pass, needs_review or fail verdict from measured checks
14
14
  (falls per minute, survival, command tracking, gait, posture), plus a vision model's read of the clip.
15
- - **Dataset augmentation.** Relight a LeRobot dataset to match a deployment scene, or generate new variations of
16
- it with video augmentation. Output is a standard LeRobot dataset (Parquet + MP4), downloaded or pushed to
17
- Hugging Face.
15
+ - **Dataset augmentation.** Relight a LeRobot dataset on Hugging Face or in a public S3 bucket to match a deployment
16
+ scene, or generate new variations of it with video augmentation. Output is a standard LeRobot dataset (Parquet +
17
+ MP4), downloaded or pushed to Hugging Face.
18
18
  - **Digital twins (beta).** Point Kite at one episode of a LeRobot dataset and get an interactable MuJoCo scene of the
19
19
  room, with the objects the robot handles built to size.
20
20
  - **Evaluations (beta).** Evaluate a policy by running a Kite SDK spec on Kite's machines, on an NVIDIA L4 for a GPU
@@ -64,7 +64,7 @@ lerobot/pusht to look like this photo".
64
64
  | --- | --- |
65
65
  | `kite_rl_catalog`, `kite_rl_plan`, `kite_rl_validate` | See which robots and objectives RL runs support, and plan and check a training spec for free |
66
66
  | `kite_rl_train`, `kite_rl_fork`, `kite_rl_cancel` | Start, branch from, and stop RL runs |
67
- | `kite_rl_status`, `kite_rl_metrics`, `kite_rl_report`, `kite_rl_list` | Follow training and read each run's evaluation verdict |
67
+ | `kite_rl_status`, `kite_rl_metrics`, `kite_rl_report`, `kite_rl_list` | Follow training and read each run's verdict |
68
68
  | `kite_augment_create`, `kite_augment_status`, `kite_augment_list`, `kite_augment_cancel` | Augment LeRobot datasets |
69
69
  | `kite_twin_validate`, `kite_twin_create`, `kite_twin_status`, `kite_twin_list`, `kite_twin_cancel`, `kite_twin_resume` | Build MuJoCo digital twins (beta) |
70
70
  | `kite_eval_validate`, `kite_eval_run`, `kite_eval_status`, `kite_eval_list`, `kite_eval_report`, `kite_eval_logs`, `kite_eval_cancel`, `kite_eval_resume` | Evaluate a policy on a Kite SDK spec on Kite's machines and read the scorecard (beta) |
@@ -90,6 +90,9 @@ kite augment create --repo-id lerobot/pusht \
90
90
  kite augment download aug_... -o ./pusht-marble
91
91
  ```
92
92
 
93
+ `--repo-id` takes a Hugging Face id (`lerobot/pusht`), its `huggingface.co/datasets/` URL, or an S3 link to the
94
+ dataset's folder in a public bucket (`s3://bucket/dataset/`).
95
+
93
96
  Build a digital twin (beta, enabled per account):
94
97
 
95
98
  ```bash
@@ -103,16 +106,20 @@ kite eval validate specs/smoke.yaml # free: the plan and what a run r
103
106
  kite eval run specs/smoke.yaml --out ./runs # -> ./runs/eval_.../scorecard.json
104
107
  ```
105
108
 
109
+ A file too big to send, such as a checkpoint's weights, can come from the Hugging Face Hub or a public S3 bucket:
110
+ `--hf PATH=OWNER/NAME@COMMIT:FILE` or `--hf PATH=<the file's huggingface.co URL>`, and `--s3 PATH=s3://BUCKET/KEY`,
111
+ PATH being the path the spec names.
112
+
106
113
  Every command prints JSON (`{"ok": true, "data": ...}`) so scripts and agents can parse it. Run `kite --help` for
107
114
  the full list.
108
115
 
109
116
  ## Links
110
117
 
111
- - Docs: [CLI and MCP](https://kite-ml.mintlify.app/tools/cli-and-mcp) ·
112
- [RL runs](https://kite-ml.mintlify.app/platform-api/rl-runs) ·
113
- [Augmentations](https://kite-ml.mintlify.app/platform-api/augmentation) ·
114
- [Twins](https://kite-ml.mintlify.app/platform-api/twins) ·
115
- [Evaluations](https://kite-ml.mintlify.app/platform-api/evaluations)
118
+ - Docs: [CLI and MCP](https://docs.kiteml.com/tools/cli-and-mcp) ·
119
+ [RL runs](https://docs.kiteml.com/platform-api/rl-runs) ·
120
+ [Augmentations](https://docs.kiteml.com/platform-api/augmentation) ·
121
+ [Twins](https://docs.kiteml.com/platform-api/twins) ·
122
+ [Evaluations](https://docs.kiteml.com/platform-api/evaluations)
116
123
  - Dashboard: [app.kiteml.com](https://app.kiteml.com)
117
124
  - Website: [kiteml.com](https://kiteml.com)
118
125
  - Support: [raul@kiteml.com](mailto:raul@kiteml.com)
@@ -43,8 +43,8 @@ dependencies = [
43
43
 
44
44
  [project.urls]
45
45
  Homepage = "https://kiteml.com"
46
- Documentation = "https://kite-ml.mintlify.app/tools/cli-and-mcp"
47
- "API reference" = "https://kite-ml.mintlify.app/platform-api/overview"
46
+ Documentation = "https://docs.kiteml.com/tools/cli-and-mcp"
47
+ "API reference" = "https://docs.kiteml.com/platform-api/overview"
48
48
  Dashboard = "https://app.kiteml.com"
49
49
  "MCP server" = "https://mcp.kiteml.com/mcp"
50
50
  Blog = "https://kiteml.com/blog"
@@ -1,3 +1,3 @@
1
1
  """Kite CLI and MCP server for the Kite robotics training platform."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.5.0"
@@ -38,10 +38,11 @@ def reference_image(path: str, camera: Optional[str] = None) -> dict:
38
38
 
39
39
 
40
40
  def scene_config(episodes, model: Optional[str], variants: Optional[int] = None,
41
- preview: Optional[bool] = None, allow_video_fallback: Optional[bool] = None) -> dict:
42
- """The /v1 ``config`` for an episode selection ("all" or a count) and the scene engines' options: background /
43
- world augment whole datasets and take ``episodes``; every other model takes a count as ``episode_count`` ("all"
44
- sends none: relight covers every episode, video and auto keep their default)."""
41
+ preview: Optional[bool] = None, allow_video_fallback: Optional[bool] = None,
42
+ video_check: Optional[str] = None) -> dict:
43
+ """The /v1 ``config`` for an episode selection ("all" or a count), the scene engines' options and the video
44
+ check's mode: background / world augment whole datasets and take ``episodes``; every other model takes a count as
45
+ ``episode_count`` ("all" sends none: relight covers every episode, video and auto keep their default)."""
45
46
  config: dict = {}
46
47
  if episodes is not None:
47
48
  if str(episodes) != "all" and not str(episodes).isdigit():
@@ -56,12 +57,14 @@ def scene_config(episodes, model: Optional[str], variants: Optional[int] = None,
56
57
  config["preview_first"] = preview
57
58
  if allow_video_fallback:
58
59
  config["allow_video_fallback"] = True
60
+ if video_check:
61
+ config["video_check"] = video_check
59
62
  return config
60
63
 
61
64
 
62
65
  @app.command()
63
66
  def create(
64
- repo_id: str = typer.Option(..., "--repo-id", help="Source HF dataset id, e.g. lerobot/pusht"),
67
+ repo_id: str = typer.Option(..., "--repo-id", help="Source LeRobot dataset: a HF id (lerobot/pusht) or huggingface.co/datasets/ URL, or an S3 link (s3://bucket/dataset/ or its https URL; public buckets)"),
65
68
  instructions: str = typer.Option(..., "-i", "--instructions", help="Augmentation instruction"),
66
69
  episodes: Optional[str] = typer.Option(
67
70
  None, "-n", "--episodes",
@@ -89,6 +92,10 @@ def create(
89
92
  help="auto: use generative video (far more tokens, no pixel guarantee) where no scene engine can run, "
90
93
  "instead of failing",
91
94
  ),
95
+ video_check: Optional[str] = typer.Option(
96
+ None, "--video-check", help="Generative video: standard (default: rejects a changed robot or a missing object; "
97
+ "the rest ship as warnings) | strict (every problem rejects) | off (records warnings, never rejects)",
98
+ ),
92
99
  reference_images: Optional[List[str]] = typer.Option(
93
100
  None, "--reference-image", help="Photo of the target look, [camera=]PATH (repeatable)"
94
101
  ),
@@ -107,7 +114,7 @@ def create(
107
114
  body: dict = {
108
115
  "source": {"type": "huggingface", "repo_id": repo_id},
109
116
  "instructions": instructions,
110
- "config": scene_config(episodes, model, variants, preview, allow_video_fallback),
117
+ "config": scene_config(episodes, model, variants, preview, allow_video_fallback, video_check),
111
118
  "output": {"type": output, "private": private},
112
119
  }
113
120
  if hf_repo:
@@ -16,7 +16,8 @@ machine otherwise. (Until 2026-10-18 this was `kite sim`, over /v1/sim_runs.)
16
16
  SPEC is the file `kite run` takes (kite_spec_version "2"). The command sends its text, asks the API which paths it
17
17
  names (`files_required`), and sends each from this disk: a file or folder Kite ships by its sha256 alone, anything
18
18
  else inline (about 15 MB in all). A file on the Hugging Face Hub, such as a checkpoint's weights, goes as a
19
- reference: `--hf PATH=OWNER/NAME@COMMIT:FILE`, PATH as the spec names it (a folder's file as FOLDER/FILE).
19
+ reference: `--hf PATH=OWNER/NAME@COMMIT:FILE`, PATH as the spec names it (a folder's file as FOLDER/FILE); an object
20
+ in a public S3 bucket as `--s3 PATH=s3://BUCKET/KEY`.
20
21
  Where runs go: --out DIR, else $KITE_EVAL_RUNS_DIR, else ./runs (`~` works). Paths printed are absolute. A run
21
22
  directory extracts to <out>/<run_id>/; the SDK's `kite verify <out>/<run_id> --evals DIR` checks it, DIR being the
22
23
  evaluators the spec named (the run records where its job read them, not your folder).
@@ -56,7 +57,10 @@ _HF = re.compile(r"(?P<repo>[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]
56
57
  r":(?P<path>.+)")
57
58
 
58
59
  _SET_HELP = "FIELD=VALUE, as `kite run --set` (repeatable)"
59
- _HF_HELP = "PATH=OWNER/NAME@COMMIT:FILE: the spec's PATH is that file of a public Hugging Face repo (repeatable)"
60
+ _HF_HELP = ("PATH=OWNER/NAME@COMMIT:FILE, or PATH=URL (https://huggingface.co/OWNER/NAME/blob/REVISION/FILE; Kite pins a "
61
+ "branch to its commit): the spec's PATH is that file of a public Hugging Face repo (repeatable)")
62
+ _S3_HELP = ("PATH=s3://BUCKET/KEY (or its https://BUCKET.s3.amazonaws.com/KEY URL): the spec's PATH is that object of a "
63
+ "public S3 bucket, pinned to the object it is when the run is created (repeatable)")
60
64
 
61
65
 
62
66
  @app.command()
@@ -64,12 +68,13 @@ def validate(
64
68
  spec: Path = typer.Argument(..., help="The spec file, e.g. specs/smoke.yaml"),
65
69
  set_: Optional[List[str]] = typer.Option(None, "--set", help=_SET_HELP),
66
70
  hf: Optional[List[str]] = typer.Option(None, "--hf", help=_HF_HELP),
71
+ s3: Optional[List[str]] = typer.Option(None, "--s3", help=_S3_HELP),
67
72
  kite_version: Optional[str] = typer.Option(None, "--kite-version", help="The Kite release you expect"),
68
73
  ):
69
74
  """Free dry run of a run: the spec plans, every file it names is sent, and the price. Creates nothing."""
70
75
  try:
71
76
  client = KiteClient(timeout=_SLOW_S)
72
- _, validation, sent = request(client, spec, set_ or [], hf_refs(hf or []), kite_version)
77
+ _, validation, sent = request(client, spec, set_ or [], {**hf_refs(hf or []), **s3_refs(s3 or [])}, kite_version)
73
78
  print_json({"ok": True, "data": validation, "files": sent})
74
79
  except KiteAPIError as e:
75
80
  print_error(e.message, code=e.code, details=e.details)
@@ -80,6 +85,7 @@ def run(
80
85
  spec: Path = typer.Argument(..., help="The spec file, e.g. specs/smoke.yaml"),
81
86
  set_: Optional[List[str]] = typer.Option(None, "--set", help=_SET_HELP),
82
87
  hf: Optional[List[str]] = typer.Option(None, "--hf", help=_HF_HELP),
88
+ s3: Optional[List[str]] = typer.Option(None, "--s3", help=_S3_HELP),
83
89
  max_minutes: Optional[int] = typer.Option(
84
90
  None, "--max-minutes", help="The run's cap, 1-720: what it reserves and the most it can cost "
85
91
  "(default 60 on CPU, 120 on an L4)"),
@@ -100,7 +106,7 @@ def run(
100
106
  key = idempotency_key or str(uuid.uuid4())
101
107
  try:
102
108
  client = KiteClient(timeout=_SLOW_S)
103
- body, _, _ = request(client, spec, set_ or [], hf_refs(hf or []), kite_version)
109
+ body, _, _ = request(client, spec, set_ or [], {**hf_refs(hf or []), **s3_refs(s3 or [])}, kite_version)
104
110
  options = {k: v for k, v in (("max_minutes", max_minutes), ("no_eval", no_eval),
105
111
  ("verify_determinism", verify_determinism)) if v}
106
112
  body = {**body, **({"options": options} if options else {}), **({"label": label} if label else {})}
@@ -242,10 +248,11 @@ def resume(run_id: str = typer.Argument(..., help="Evaluation id (eval_...)")):
242
248
 
243
249
  # --- the request: the spec and every path it names ------------------------------------------------------------------
244
250
 
245
- def request(client: KiteClient, spec: Path, overrides: List[str], hf: Dict[str, dict],
251
+ def request(client: KiteClient, spec: Path, overrides: List[str], remote: Dict[str, dict],
246
252
  kite_version: Optional[str] = None) -> Tuple[dict, dict, Dict[str, str]]:
247
253
  """The request body for SPEC with every path it names, the validation that accepted it, and how each path went
248
- (`sha256`, `inline`, `hf`).
254
+ (`sha256`, `inline`, `hf`, `s3`). `remote` maps the paths that are an `hf` file or an `s3` object to their
255
+ reference ({"hf": …} or {"s3": …}).
249
256
 
250
257
  The API names what it lacks: `files_required` lists the paths the spec names that the request does not carry,
251
258
  and `file_unavailable` on `files.<path>` says a hash Kite does not ship. A path first goes by its hash alone, so
@@ -253,12 +260,12 @@ def request(client: KiteClient, spec: Path, overrides: List[str], hf: Dict[str,
253
260
  text, spec_path, base = _spec(spec)
254
261
  files: Dict[str, dict] = {}
255
262
  sent: Dict[str, str] = {}
256
- for key, ref in hf.items():
257
- files[key] = {"hf": ref}
263
+ for key, ref in remote.items():
264
+ files[key] = dict(ref)
258
265
  local = _local(base, key)
259
- if local.is_file(): # pin the bytes this machine has: the Hub's file must be the same
266
+ if local.is_file(): # pin the bytes this machine has: the Hub's file or the object must be the same
260
267
  files[key]["sha256"] = _file_sha256(local)
261
- sent[key] = "hf"
268
+ sent[key] = next(iter(ref))
262
269
  body: dict = {"spec_yaml": text, "spec_path": spec_path, "files": files}
263
270
  if overrides:
264
271
  body["overrides"] = list(overrides)
@@ -356,8 +363,8 @@ def _fits(key: str, raw_bytes: int, files: Dict[str, dict], biggest: List[Tuple[
356
363
  if size > _INLINE_MAX or total > _BODY_MAX:
357
364
  hint = "".join(f", {posixpath.join(key, rel)} {p.stat().st_size >> 20} MB" for rel, p in biggest)
358
365
  raise KiteAPIError(413, f"{key} is not a file Kite ships, and it is too big to send inline "
359
- f"({raw_bytes >> 20} MB{hint}): put its large files on the Hugging Face Hub and "
360
- "pass each with --hf PATH=OWNER/NAME@COMMIT:FILE", code="file_too_large",
366
+ f"({raw_bytes >> 20} MB{hint}): put its large files on the Hugging Face Hub or in a public "
367
+ "S3 bucket and pass each with --hf PATH=OWNER/NAME@COMMIT:FILE or --s3 PATH=s3://BUCKET/KEY", code="file_too_large",
361
368
  details={"path": key})
362
369
 
363
370
 
@@ -401,11 +408,28 @@ def hf_refs(pairs: List[str]) -> Dict[str, dict]:
401
408
  refs: Dict[str, dict] = {}
402
409
  for pair in pairs:
403
410
  key, _, ref = pair.partition("=")
404
- m = _HF.fullmatch(ref.strip())
411
+ ref = ref.strip()
412
+ if key and re.match(r"(https?://)?(www\.)?(huggingface|hf)\.co/", ref): # the API reads the URL
413
+ refs[key.strip()] = {"hf": ref}
414
+ continue
415
+ m = _HF.fullmatch(ref)
405
416
  if not key or not m:
406
417
  raise KiteAPIError(400, f"--hf {pair!r}: expected PATH=OWNER/NAME@COMMIT:FILE, with the full 40-character "
407
- "commit", code="parameter_invalid", details={"param": "hf"})
408
- refs[key.strip()] = {"repo": m["repo"], "revision": m["revision"], "path": m["path"]}
418
+ "commit, or PATH=the file's huggingface.co URL", code="parameter_invalid",
419
+ details={"param": "hf"})
420
+ refs[key.strip()] = {"hf": {"repo": m["repo"], "revision": m["revision"], "path": m["path"]}}
421
+ return refs
422
+
423
+
424
+ def s3_refs(pairs: List[str]) -> Dict[str, dict]:
425
+ """PATH=S3_LINK pairs as {"s3": link} references; the API reads the link."""
426
+ refs: Dict[str, dict] = {}
427
+ for pair in pairs:
428
+ key, _, link = pair.partition("=")
429
+ if not key or not re.match(r"(?i)(s3://|https://)\S+", link.strip()):
430
+ raise KiteAPIError(400, f"--s3 {pair!r}: expected PATH=s3://BUCKET/KEY", code="parameter_invalid",
431
+ details={"param": "s3"})
432
+ refs[key.strip()] = {"s3": link.strip()}
409
433
  return refs
410
434
 
411
435
 
@@ -161,6 +161,17 @@ class KiteClient:
161
161
  for chunk in resp.iter_bytes(chunk_size=8192):
162
162
  f.write(chunk)
163
163
 
164
+ def redirect_target(self, path: str) -> Optional[str]:
165
+ """Where an API download path (a run's or a twin's archive) redirects: a fresh signed link, or None when the
166
+ API streams the file itself. Only the response's headers are read."""
167
+ with self._client.stream("GET", path, headers=self._base_headers(), follow_redirects=False) as resp:
168
+ if resp.is_redirect:
169
+ return resp.headers.get("location")
170
+ if resp.status_code >= 400:
171
+ resp.read()
172
+ raise _api_error(resp)
173
+ return None
174
+
164
175
  def close(self) -> None:
165
176
  self._client.close()
166
177
 
@@ -56,6 +56,23 @@ def _twin_source(dataset: str) -> dict:
56
56
  return {"type": "huggingface", "repo_id": dataset}
57
57
 
58
58
 
59
+ def _fetch_command(d: dict, fname: str, client: KiteClient) -> str:
60
+ """The command that fetches a finished resource's archive. From Kite-Version 2026-10-12 `download.url` is this
61
+ API's path, which needs the key: ask it for the fresh signed link it redirects to, so an agent without the key
62
+ (the hosted server's OAuth) can fetch it directly. Without one, the API path with the key."""
63
+ if d.get("requires_auth"):
64
+ resolve = getattr(client, "redirect_target", None)
65
+ try:
66
+ signed = resolve(d["url"]) if resolve else None
67
+ except Exception: # the link is a convenience: the API path below still works
68
+ signed = None
69
+ if signed:
70
+ return f'curl -fL "{signed}" -o {fname}'
71
+ url = f"{client.base_url.rstrip('/')}{d['url']}"
72
+ return f'curl -fL -H "Authorization: Bearer $KITE_API_KEY" "{url}" -o {fname}'
73
+ return f'curl -fL "{d["url"]}" -o {fname}'
74
+
75
+
59
76
  def _twin_view(twin: dict, client: KiteClient) -> dict:
60
77
  """A twin as an agent should see it: the resource plus `next`, the one thing to do now. A hosted MCP server
61
78
  cannot write to the agent's disk, so a finished twin's `next` is the exact command that fetches and
@@ -65,11 +82,7 @@ def _twin_view(twin: dict, client: KiteClient) -> dict:
65
82
  d = twin.get("download") or {}
66
83
  if status == "succeeded" and d:
67
84
  fname = d.get("filename") or f"{tid}.tar.gz"
68
- if d.get("requires_auth"):
69
- url = f"{client.base_url.rstrip('/')}{d['url']}"
70
- fetch = f'curl -fL -H "Authorization: Bearer $KITE_API_KEY" "{url}" -o {fname}'
71
- else:
72
- fetch = f'curl -fL "{d["url"]}" -o {fname}'
85
+ fetch = _fetch_command(d, fname, client)
73
86
  out["next"] = (f"{fetch} && tar xzf {fname} # -> {d.get('scene')}"
74
87
  + (f" (link valid until {d['expires_at']}; call kite_twin_status again for a fresh one)"
75
88
  if d.get("expires_at") else ""))
@@ -95,11 +108,7 @@ def _eval_view(run: dict, client: KiteClient) -> dict:
95
108
  d = run.get("download") or {}
96
109
  if status == "succeeded" and d:
97
110
  fname = d.get("filename") or f"{rid}.tar.gz"
98
- if d.get("requires_auth"):
99
- url = f"{client.base_url.rstrip('/')}{d['url']}"
100
- fetch = f'curl -fL -H "Authorization: Bearer $KITE_API_KEY" "{url}" -o {fname}'
101
- else:
102
- fetch = f'curl -fL "{d["url"]}" -o {fname}'
111
+ fetch = _fetch_command(d, fname, client)
103
112
  out["next"] = (f"{fetch} && tar xzf {fname} # -> {rid}/, the run directory (`kite verify {rid} --evals <the spec's evaluators>` checks it)"
104
113
  + (f" (link valid until {d['expires_at']}; call kite_eval_status again for a fresh one)"
105
114
  if d.get("expires_at") else "") + f"; kite_eval_report {rid} has the scorecard")
@@ -131,10 +140,11 @@ def _eval_request(args: dict, client: KiteClient) -> tuple[dict, dict | None]:
131
140
  raise ValueError("`spec` only works with the local (stdio) Kite MCP server; send `spec_yaml` and `files`")
132
141
  from pathlib import Path
133
142
 
134
- from kite_cli.cli.commands.evaluations import hf_refs, request
143
+ from kite_cli.cli.commands.evaluations import hf_refs, request, s3_refs
135
144
 
136
- hf = hf_refs([f"{k}={v}" for k, v in (args.get("hf") or {}).items()])
137
- body, validation, _ = request(client, Path(args["spec"]), args.get("overrides") or [], hf, args.get("kite_version"))
145
+ remote = {**hf_refs([f"{k}={v}" for k, v in (args.get("hf") or {}).items()]),
146
+ **s3_refs([f"{k}={v}" for k, v in (args.get("s3") or {}).items()])}
147
+ body, validation, _ = request(client, Path(args["spec"]), args.get("overrides") or [], remote, args.get("kite_version"))
138
148
  return body, validation
139
149
 
140
150
 
@@ -194,9 +204,10 @@ _EVAL_SPEC = {
194
204
  "spec": {"type": "string", "description": "A Kite SDK spec file on this machine, best as an absolute path (the local server only): every path it names is sent from this disk, a file Kite ships by its hash alone"},
195
205
  "spec_yaml": {"type": "string", "description": "The spec's text, instead of `spec` (the hosted server): send `spec_path` and `files` with it"},
196
206
  "spec_path": {"type": "string", "description": "With spec_yaml: its path in your workspace, e.g. specs/smoke.yaml. Paths in the spec are read relative to it"},
197
- "files": {"type": "object", "description": "With spec_yaml: each path the spec names, as it names it, to where its bytes come from: {\"kind\": \"directory\", \"shipped\": \"evals\"} (a path Kite ships), {\"sha256\", \"kind\"} (a shipped file by hash), {\"text\"} or {\"base64\"} (a folder as a .tar), or {\"hf\": {\"repo\", \"revision\" (a full commit), \"path\"}}. A missing one is files_required, listing each"},
207
+ "files": {"type": "object", "description": "With spec_yaml: each path the spec names, as it names it, to where its bytes come from: {\"kind\": \"directory\", \"shipped\": \"evals\"} (a path Kite ships), {\"sha256\", \"kind\"} (a shipped file by hash), {\"text\"} or {\"base64\"} (a folder as a .tar), or {\"hf\": {\"repo\", \"revision\" (a full commit), \"path\"}} or {\"hf\": \"<the file's huggingface.co URL>\"} (a branch is pinned to its commit), or {\"s3\": \"s3://BUCKET/KEY\"} (an object in a public bucket, pinned to its ETag). A missing one is files_required, listing each"},
198
208
  "overrides": {"type": "array", "items": {"type": "string"}, "description": "FIELD=VALUE pairs, as `kite run --set`, e.g. episodes.count=20. Optional"},
199
- "hf": {"type": "object", "description": "With spec: {PATH: \"OWNER/NAME@COMMIT:FILE\"}, the paths that are a public Hugging Face file (a checkpoint's weights). Optional"},
209
+ "hf": {"type": "object", "description": "With spec: {PATH: \"OWNER/NAME@COMMIT:FILE\" or the file's huggingface.co URL}, the paths that are a public Hugging Face file (a checkpoint's weights). Optional"},
210
+ "s3": {"type": "object", "description": "With spec: {PATH: \"s3://BUCKET/KEY\"}, the paths that are an object in a public S3 bucket. Optional"},
200
211
  "kite_version": {"type": "string", "description": "The Kite release you expect; refused if it is not the one served. Optional"},
201
212
  }
202
213
  _EVAL_ID = {"run_id": {"type": "string", "description": "Evaluation id (eval_...)"}}
@@ -208,9 +219,9 @@ _EVAL_ID = {"run_id": {"type": "string", "description": "Evaluation id (eval_...
208
219
  TOOLS = [
209
220
  # Augmentations (public /v1 API — relighting + video augmentation)
210
221
  Tool(name="kite_augment_create", title="Create Augmentation",
211
- description="Create a dataset augmentation run from a HuggingFace LeRobot dataset. model 'auto' (default) lets a planner pick per camera: 'relight' (cheap, exact colour/exposure/warmth transform of every episode — e.g. match a darker, warmer deployment scene), 'background' (a new place behind the work cell from a panorama; the robot, objects and table keep their recorded pixels), 'world' (a 3D world rendered from each camera's pose, for moving cameras) or 'video' (generative video augmentation of content: textures, objects). background and world run on whole datasets and may not be enabled for every account. Pass reference_images of the target look. Returns the augmentation resource with its id (aug_...) and, once planned, the chosen plan.",
222
+ description="Create a dataset augmentation run from a LeRobot dataset on HuggingFace or in a public S3 bucket. model 'auto' (default) lets a planner pick per camera: 'relight' (cheap, exact colour/exposure/warmth transform of every episode — e.g. match a darker, warmer deployment scene), 'background' (a new place behind the work cell from a panorama; the robot, objects and table keep their recorded pixels), 'world' (a 3D world rendered from each camera's pose, for moving cameras) or 'video' (generative video augmentation of content: textures, objects). background and world run on whole datasets and may not be enabled for every account. Pass reference_images of the target look. Returns the augmentation resource with its id (aug_...) and, once planned, the chosen plan.",
212
223
  inputSchema={"type": "object", "properties": {
213
- "repo_id": {"type": "string", "description": "Source HF dataset id, e.g. lerobot/pusht"},
224
+ "repo_id": {"type": "string", "description": "Source LeRobot dataset: a HuggingFace id (lerobot/pusht) or its huggingface.co/datasets/ URL, or an S3 link to its folder in a public bucket (s3://bucket/dataset/ or its https://….amazonaws.com URL)"},
214
225
  "instructions": {"type": "string", "description": "Augmentation instruction"},
215
226
  "episode_count": {"type": "integer", "description": "Video: episodes to generate (1-50, default 3)"},
216
227
  "episodes": {"anyOf": [{"type": "integer", "minimum": 1}, {"type": "string", "enum": ["all"]}],
@@ -228,6 +239,7 @@ TOOLS = [
228
239
  "variants": {"type": "integer", "description": "background/world: environments per source episode (default 1)"},
229
240
  "preview_first": {"type": "boolean", "description": "background/world: check one episode's quality gates (windows at its start, middle and end) before charging for the rest (default true)"},
230
241
  "allow_video_fallback": {"type": "boolean", "description": "auto: where no scene engine can run, use generative video (far more tokens, no pixel guarantee) instead of failing with the reasons and the video price (default false)"},
242
+ "video_check": {"type": "string", "enum": ["standard", "strict", "off"], "description": "Generative video: what the check of each clip rejects. standard (default): a changed robot or a missing, duplicated or swapped object; an object redrawn or the table or view redrawn ship as warnings. strict: every problem rejects. off: records warnings, never rejects"},
231
243
  "reference_images": {"type": "array", "maxItems": 6, "description": "Photos of the target look", "items": {
232
244
  "type": "object", "properties": {
233
245
  "camera": {"type": "string", "description": "Camera it shows (e.g. 'top'); omit for all cameras"},
@@ -419,7 +431,8 @@ async def _dispatch(name: str, args: dict, client: KiteClient) -> list[TextConte
419
431
  "source": {"type": "huggingface", "repo_id": args["repo_id"]},
420
432
  "instructions": args["instructions"],
421
433
  "config": scene_config(args.get("episodes", args.get("episode_count")), args.get("model"),
422
- args.get("variants"), args.get("preview_first"), args.get("allow_video_fallback")),
434
+ args.get("variants"), args.get("preview_first"), args.get("allow_video_fallback"),
435
+ args.get("video_check")),
423
436
  "output": {"type": args.get("output_type", "download")},
424
437
  **{k: args[k] for k in ("environments", "must_keep") if args.get(k)},
425
438
  }
File without changes