kiteml-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kiteml_cli-0.1.0/.gitignore +7 -0
- kiteml_cli-0.1.0/PKG-INFO +154 -0
- kiteml_cli-0.1.0/README.md +106 -0
- kiteml_cli-0.1.0/pyproject.toml +79 -0
- kiteml_cli-0.1.0/src/kite_cli/__init__.py +3 -0
- kiteml_cli-0.1.0/src/kite_cli/auth.py +184 -0
- kiteml_cli-0.1.0/src/kite_cli/cli/__init__.py +0 -0
- kiteml_cli-0.1.0/src/kite_cli/cli/commands/__init__.py +0 -0
- kiteml_cli-0.1.0/src/kite_cli/cli/commands/augment.py +164 -0
- kiteml_cli-0.1.0/src/kite_cli/cli/commands/auth_cmd.py +57 -0
- kiteml_cli-0.1.0/src/kite_cli/cli/commands/config_cmd.py +39 -0
- kiteml_cli-0.1.0/src/kite_cli/cli/commands/rl.py +356 -0
- kiteml_cli-0.1.0/src/kite_cli/cli/commands/twin.py +322 -0
- kiteml_cli-0.1.0/src/kite_cli/cli/main.py +88 -0
- kiteml_cli-0.1.0/src/kite_cli/client.py +180 -0
- kiteml_cli-0.1.0/src/kite_cli/config.py +80 -0
- kiteml_cli-0.1.0/src/kite_cli/mcp/__init__.py +0 -0
- kiteml_cli-0.1.0/src/kite_cli/mcp/server.py +464 -0
- kiteml_cli-0.1.0/src/kite_cli/output.py +79 -0
- kiteml_cli-0.1.0/src/kite_cli/progress.py +104 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kiteml-cli
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
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
|
|
8
|
+
Project-URL: Dashboard, https://app.kiteml.com
|
|
9
|
+
Project-URL: MCP server, https://mcp.kiteml.com/mcp
|
|
10
|
+
Project-URL: Blog, https://kiteml.com/blog
|
|
11
|
+
Author-email: Kite ML <raul@kiteml.com>
|
|
12
|
+
License-Expression: Apache-2.0
|
|
13
|
+
Keywords: ai agents,claude,cli,cursor,data augmentation,dataset augmentation,digital twin,humanoid,imitation learning,kite,legged robots,lerobot,mcp,mcp-server,model context protocol,mujoco,onnx,policy evaluation,policy training,reinforcement learning,rl,robot learning,robot policy,robotics,sim-to-real,simulation,vla
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: httpx>=0.25
|
|
30
|
+
Requires-Dist: rich>=13.0
|
|
31
|
+
Requires-Dist: typer>=0.9
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: mcp<2,>=1.0; extra == 'all'
|
|
34
|
+
Requires-Dist: starlette>=0.37; extra == 'all'
|
|
35
|
+
Requires-Dist: uvicorn>=0.30; extra == 'all'
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: mcp<2,>=1.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
41
|
+
Requires-Dist: starlette>=0.37; extra == 'dev'
|
|
42
|
+
Requires-Dist: uvicorn>=0.30; extra == 'dev'
|
|
43
|
+
Provides-Extra: mcp
|
|
44
|
+
Requires-Dist: mcp<2,>=1.0; extra == 'mcp'
|
|
45
|
+
Requires-Dist: starlette>=0.37; extra == 'mcp'
|
|
46
|
+
Requires-Dist: uvicorn>=0.30; extra == 'mcp'
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# Kite CLI and MCP server
|
|
50
|
+
|
|
51
|
+
Robot learning on cloud GPUs, from your terminal or your AI agent. Kite trains robot policies with
|
|
52
|
+
reinforcement learning in simulation, augments LeRobot datasets, and rebuilds robot episodes as MuJoCo digital
|
|
53
|
+
twins. This package gives you the `kite` command line and the `kite-mcp` server for Claude, Cursor, and any
|
|
54
|
+
Model Context Protocol (MCP) client.
|
|
55
|
+
|
|
56
|
+
<!-- mcp-name: com.kiteml/kite -->
|
|
57
|
+
|
|
58
|
+
- **RL training in simulation.** Describe a behavior in plain English. Kite writes the task spec, checks its
|
|
59
|
+
rewards for free, and trains the policy on a GPU. You get the policy as ONNX, the MuJoCo scene it trained in, and
|
|
60
|
+
clips.
|
|
61
|
+
- **Policy evaluation.** Every RL run ends with a report: a pass, needs_review or fail verdict from measured checks
|
|
62
|
+
(falls per minute, survival, command tracking, gait, posture), plus a vision model's read of the clip.
|
|
63
|
+
- **Dataset augmentation.** Relight a LeRobot dataset to match a deployment scene, or generate new variations of
|
|
64
|
+
it with video augmentation. Output is a standard LeRobot dataset (Parquet + MP4), downloaded or pushed to
|
|
65
|
+
Hugging Face.
|
|
66
|
+
- **Digital twins.** Point Kite at one episode of a LeRobot dataset and get an interactable MuJoCo scene of the
|
|
67
|
+
room, with the objects the robot handles built to size.
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install "kiteml-cli[mcp]"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Python 3.10 or later. Drop `[mcp]` if you only want the CLI.
|
|
76
|
+
|
|
77
|
+
## Sign in
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
kite auth login # opens the dashboard; stores a 90-day API key
|
|
81
|
+
kite doctor # checks the API is reachable and your key works
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
On a server or in CI, set an API key from [app.kiteml.com](https://app.kiteml.com) instead:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export KITE_API_KEY=kite_...
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Use it from an AI agent
|
|
91
|
+
|
|
92
|
+
Kite hosts the MCP server, so there's nothing to run:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
claude mcp add --transport http kite https://mcp.kiteml.com/mcp
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or run it locally over stdio with this package:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
claude mcp add kite -e KITE_API_KEY=$KITE_API_KEY -- kite-mcp
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
For Cursor and other clients, add `{"mcpServers": {"kite": {"url": "https://mcp.kiteml.com/mcp"}}}` to the MCP
|
|
105
|
+
config. Then ask, for example: "train the Open Duck Mini to walk forward and tell me when it passes", or "relight
|
|
106
|
+
lerobot/pusht to look like this photo".
|
|
107
|
+
|
|
108
|
+
| Tools | What they do |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `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 |
|
|
111
|
+
| `kite_rl_train`, `kite_rl_fork`, `kite_rl_cancel` | Start, branch from, and stop RL runs |
|
|
112
|
+
| `kite_rl_status`, `kite_rl_metrics`, `kite_rl_report`, `kite_rl_list` | Follow training and read each run's evaluation verdict |
|
|
113
|
+
| `kite_augment_create`, `kite_augment_status`, `kite_augment_list`, `kite_augment_cancel` | Augment LeRobot datasets |
|
|
114
|
+
| `kite_twin_validate`, `kite_twin_create`, `kite_twin_status`, `kite_twin_list`, `kite_twin_cancel`, `kite_twin_resume` | Build MuJoCo digital twins |
|
|
115
|
+
| `kite_doctor` | Check connectivity and authentication |
|
|
116
|
+
|
|
117
|
+
## Use it from the terminal
|
|
118
|
+
|
|
119
|
+
Train a walking policy for the Open Duck Mini v2:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
kite rl plan open_duck_mini_v2 "walk forward at a steady pace" -o walk.json
|
|
123
|
+
kite rl validate walk.json # free: what each reward term pays canned policies
|
|
124
|
+
kite rl train walk.json --budget probe --wait
|
|
125
|
+
kite rl report rlr_... # the verdict and each check behind it
|
|
126
|
+
kite rl download rlr_... # -> kiteml_rlr_.../policy/policy.onnx
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Augment a dataset:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
kite augment create --repo-id lerobot/pusht \
|
|
133
|
+
-i "change the table surface to white marble, vary the lighting" -n 20 --wait
|
|
134
|
+
kite augment download aug_... -o ./pusht-marble
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Build a digital twin (private beta):
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
kite twin create lerobot/svla_so101_pickplace --out ./twins # -> ./twins/twin_.../scene.xml
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Every command prints JSON (`{"ok": true, "data": ...}`) so scripts and agents can parse it. Run `kite --help` for
|
|
144
|
+
the full list.
|
|
145
|
+
|
|
146
|
+
## Links
|
|
147
|
+
|
|
148
|
+
- Docs: [CLI and MCP](https://kite-ml.mintlify.app/tools/cli-and-mcp) ·
|
|
149
|
+
[RL runs](https://kite-ml.mintlify.app/platform-api/rl-runs) ·
|
|
150
|
+
[Augmentations](https://kite-ml.mintlify.app/platform-api/augmentation) ·
|
|
151
|
+
[Twins](https://kite-ml.mintlify.app/platform-api/twins)
|
|
152
|
+
- Dashboard: [app.kiteml.com](https://app.kiteml.com)
|
|
153
|
+
- Website: [kiteml.com](https://kiteml.com)
|
|
154
|
+
- Support: [raul@kiteml.com](mailto:raul@kiteml.com)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Kite CLI and MCP server
|
|
2
|
+
|
|
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
|
|
6
|
+
Model Context Protocol (MCP) client.
|
|
7
|
+
|
|
8
|
+
<!-- mcp-name: com.kiteml/kite -->
|
|
9
|
+
|
|
10
|
+
- **RL training in simulation.** Describe a behavior in plain English. Kite writes the task spec, checks its
|
|
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
|
+
clips.
|
|
13
|
+
- **Policy evaluation.** Every RL run ends with a report: a pass, needs_review or fail verdict from measured checks
|
|
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.
|
|
18
|
+
- **Digital twins.** Point Kite at one episode of a LeRobot dataset and get an interactable MuJoCo scene of the
|
|
19
|
+
room, with the objects the robot handles built to size.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install "kiteml-cli[mcp]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Python 3.10 or later. Drop `[mcp]` if you only want the CLI.
|
|
28
|
+
|
|
29
|
+
## Sign in
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
kite auth login # opens the dashboard; stores a 90-day API key
|
|
33
|
+
kite doctor # checks the API is reachable and your key works
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
On a server or in CI, set an API key from [app.kiteml.com](https://app.kiteml.com) instead:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export KITE_API_KEY=kite_...
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Use it from an AI agent
|
|
43
|
+
|
|
44
|
+
Kite hosts the MCP server, so there's nothing to run:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
claude mcp add --transport http kite https://mcp.kiteml.com/mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or run it locally over stdio with this package:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude mcp add kite -e KITE_API_KEY=$KITE_API_KEY -- kite-mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For Cursor and other clients, add `{"mcpServers": {"kite": {"url": "https://mcp.kiteml.com/mcp"}}}` to the MCP
|
|
57
|
+
config. Then ask, for example: "train the Open Duck Mini to walk forward and tell me when it passes", or "relight
|
|
58
|
+
lerobot/pusht to look like this photo".
|
|
59
|
+
|
|
60
|
+
| Tools | What they do |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `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 |
|
|
63
|
+
| `kite_rl_train`, `kite_rl_fork`, `kite_rl_cancel` | Start, branch from, and stop RL runs |
|
|
64
|
+
| `kite_rl_status`, `kite_rl_metrics`, `kite_rl_report`, `kite_rl_list` | Follow training and read each run's evaluation verdict |
|
|
65
|
+
| `kite_augment_create`, `kite_augment_status`, `kite_augment_list`, `kite_augment_cancel` | Augment LeRobot datasets |
|
|
66
|
+
| `kite_twin_validate`, `kite_twin_create`, `kite_twin_status`, `kite_twin_list`, `kite_twin_cancel`, `kite_twin_resume` | Build MuJoCo digital twins |
|
|
67
|
+
| `kite_doctor` | Check connectivity and authentication |
|
|
68
|
+
|
|
69
|
+
## Use it from the terminal
|
|
70
|
+
|
|
71
|
+
Train a walking policy for the Open Duck Mini v2:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
kite rl plan open_duck_mini_v2 "walk forward at a steady pace" -o walk.json
|
|
75
|
+
kite rl validate walk.json # free: what each reward term pays canned policies
|
|
76
|
+
kite rl train walk.json --budget probe --wait
|
|
77
|
+
kite rl report rlr_... # the verdict and each check behind it
|
|
78
|
+
kite rl download rlr_... # -> kiteml_rlr_.../policy/policy.onnx
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Augment a dataset:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
kite augment create --repo-id lerobot/pusht \
|
|
85
|
+
-i "change the table surface to white marble, vary the lighting" -n 20 --wait
|
|
86
|
+
kite augment download aug_... -o ./pusht-marble
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Build a digital twin (private beta):
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
kite twin create lerobot/svla_so101_pickplace --out ./twins # -> ./twins/twin_.../scene.xml
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Every command prints JSON (`{"ok": true, "data": ...}`) so scripts and agents can parse it. Run `kite --help` for
|
|
96
|
+
the full list.
|
|
97
|
+
|
|
98
|
+
## Links
|
|
99
|
+
|
|
100
|
+
- Docs: [CLI and MCP](https://kite-ml.mintlify.app/tools/cli-and-mcp) ·
|
|
101
|
+
[RL runs](https://kite-ml.mintlify.app/platform-api/rl-runs) ·
|
|
102
|
+
[Augmentations](https://kite-ml.mintlify.app/platform-api/augmentation) ·
|
|
103
|
+
[Twins](https://kite-ml.mintlify.app/platform-api/twins)
|
|
104
|
+
- Dashboard: [app.kiteml.com](https://app.kiteml.com)
|
|
105
|
+
- Website: [kiteml.com](https://kiteml.com)
|
|
106
|
+
- Support: [raul@kiteml.com](mailto:raul@kiteml.com)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kiteml-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "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."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Kite ML", email = "raul@kiteml.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"robotics", "robot learning", "reinforcement learning", "rl", "sim-to-real", "simulation", "mujoco",
|
|
15
|
+
"robot policy", "policy training", "policy evaluation", "lerobot", "imitation learning", "vla",
|
|
16
|
+
"data augmentation", "dataset augmentation", "digital twin", "humanoid", "legged robots", "onnx",
|
|
17
|
+
"mcp", "mcp-server", "model context protocol", "ai agents", "claude", "cursor", "cli", "kite",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Environment :: Console",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Intended Audience :: Science/Research",
|
|
24
|
+
"License :: OSI Approved :: Apache Software License",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
33
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
dependencies = [
|
|
37
|
+
"typer>=0.9",
|
|
38
|
+
"httpx>=0.25",
|
|
39
|
+
"rich>=13.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://kiteml.com"
|
|
44
|
+
Documentation = "https://kite-ml.mintlify.app/tools/cli-and-mcp"
|
|
45
|
+
"API reference" = "https://kite-ml.mintlify.app/platform-api/overview"
|
|
46
|
+
Dashboard = "https://app.kiteml.com"
|
|
47
|
+
"MCP server" = "https://mcp.kiteml.com/mcp"
|
|
48
|
+
Blog = "https://kiteml.com/blog"
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
mcp = ["mcp>=1.0,<2", "uvicorn>=0.30", "starlette>=0.37"]
|
|
52
|
+
all = ["kiteml-cli[mcp]"]
|
|
53
|
+
dev = [
|
|
54
|
+
"kiteml-cli[all]",
|
|
55
|
+
"pytest>=7.0",
|
|
56
|
+
"pytest-asyncio",
|
|
57
|
+
"ruff",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[project.scripts]
|
|
61
|
+
kite = "kite_cli.cli.main:app"
|
|
62
|
+
kite-mcp = "kite_cli.mcp.server:main"
|
|
63
|
+
|
|
64
|
+
# The published package is the public /v1 client. kite_cli/internal (the dashboard's commands and tools, and the
|
|
65
|
+
# hosted server's HTTP transport) stays in this repo: the hosted image puts the source tree on PYTHONPATH instead.
|
|
66
|
+
[tool.hatch.build.targets.wheel]
|
|
67
|
+
packages = ["src/kite_cli"]
|
|
68
|
+
exclude = ["src/kite_cli/internal"]
|
|
69
|
+
|
|
70
|
+
[tool.hatch.build.targets.sdist]
|
|
71
|
+
only-include = ["src/kite_cli", "README.md", "pyproject.toml"]
|
|
72
|
+
exclude = ["src/kite_cli/internal"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff]
|
|
75
|
+
line-length = 120
|
|
76
|
+
target-version = "py310"
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication for the Kite CLI.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- API key: KITE_API_KEY env var (recommended for MCP / programmatic access)
|
|
6
|
+
- Browser OAuth: `kite auth login` opens the dashboard in a browser → an API key is stored
|
|
7
|
+
- Environment variable: KITE_TOKEN for CI/headless
|
|
8
|
+
- Token file: ~/.kite/credentials.json (written by login, read by client)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import secrets
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
import webbrowser
|
|
20
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
from urllib.parse import parse_qs, urlparse
|
|
24
|
+
|
|
25
|
+
from kite_cli.config import KITE_DIR, get_app_url
|
|
26
|
+
|
|
27
|
+
CREDENTIALS_PATH = KITE_DIR / "credentials.json"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_token() -> Optional[str]:
|
|
31
|
+
"""Get a valid auth token. Checks env vars first, then credentials file."""
|
|
32
|
+
# 1. API key (MCP / programmatic access)
|
|
33
|
+
api_key = os.environ.get("KITE_API_KEY")
|
|
34
|
+
if api_key:
|
|
35
|
+
return api_key
|
|
36
|
+
|
|
37
|
+
# 2. Token override (CI, headless)
|
|
38
|
+
env_token = os.environ.get("KITE_TOKEN")
|
|
39
|
+
if env_token:
|
|
40
|
+
return env_token
|
|
41
|
+
|
|
42
|
+
# 3. Stored credentials
|
|
43
|
+
creds = _load_credentials()
|
|
44
|
+
if creds and creds.get("id_token"):
|
|
45
|
+
return creds["id_token"]
|
|
46
|
+
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def save_credentials(id_token: str, user_info: Optional[dict] = None) -> None:
|
|
51
|
+
"""Save credentials to disk with restricted permissions."""
|
|
52
|
+
KITE_DIR.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
creds = {
|
|
54
|
+
"id_token": id_token,
|
|
55
|
+
"user_info": user_info,
|
|
56
|
+
"saved_at": time.time(),
|
|
57
|
+
}
|
|
58
|
+
CREDENTIALS_PATH.write_text(json.dumps(creds, indent=2) + "\n")
|
|
59
|
+
# Restrict file permissions (owner read/write only)
|
|
60
|
+
try:
|
|
61
|
+
CREDENTIALS_PATH.chmod(0o600)
|
|
62
|
+
except OSError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def clear_credentials() -> None:
|
|
67
|
+
"""Remove stored credentials."""
|
|
68
|
+
if CREDENTIALS_PATH.exists():
|
|
69
|
+
CREDENTIALS_PATH.unlink()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _load_credentials() -> Optional[dict]:
|
|
73
|
+
"""Load credentials from disk."""
|
|
74
|
+
if not CREDENTIALS_PATH.exists():
|
|
75
|
+
return None
|
|
76
|
+
try:
|
|
77
|
+
return json.loads(CREDENTIALS_PATH.read_text())
|
|
78
|
+
except (json.JSONDecodeError, OSError):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
83
|
+
"""HTTP handler that receives the OAuth callback with the token."""
|
|
84
|
+
|
|
85
|
+
token: Optional[str] = None
|
|
86
|
+
user_info: Optional[dict] = None
|
|
87
|
+
expected_state: Optional[str] = None
|
|
88
|
+
|
|
89
|
+
def do_GET(self):
|
|
90
|
+
parsed = urlparse(self.path)
|
|
91
|
+
params = parse_qs(parsed.query)
|
|
92
|
+
|
|
93
|
+
if parsed.path != "/callback":
|
|
94
|
+
self.send_response(404)
|
|
95
|
+
self.end_headers()
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
state = params.get("state", [None])[0]
|
|
99
|
+
token = params.get("token", [None])[0]
|
|
100
|
+
email = params.get("email", [None])[0]
|
|
101
|
+
|
|
102
|
+
# Only accept the login this process started. Without this, any local
|
|
103
|
+
# page could hit the ephemeral port and plant a token. compare_digest
|
|
104
|
+
# keeps the nonce from being probed a byte at a time.
|
|
105
|
+
expected = _CallbackHandler.expected_state
|
|
106
|
+
if not expected or not state or not secrets.compare_digest(state, expected):
|
|
107
|
+
self._reply(403, "Login failed", "This callback did not come from the login you started.")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if not token:
|
|
111
|
+
self._reply(400, "Login failed", "No token received.")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
_CallbackHandler.token = token
|
|
115
|
+
_CallbackHandler.user_info = {"email": email} if email else None
|
|
116
|
+
self._reply(200, "Login successful!", "You can close this tab.")
|
|
117
|
+
|
|
118
|
+
def _reply(self, status: int, heading: str, detail: str) -> None:
|
|
119
|
+
self.send_response(status)
|
|
120
|
+
self.send_header("Content-Type", "text/html")
|
|
121
|
+
self.end_headers()
|
|
122
|
+
self.wfile.write(f"<html><body><h2>{heading}</h2><p>{detail}</p></body></html>".encode())
|
|
123
|
+
|
|
124
|
+
def log_message(self, format, *args):
|
|
125
|
+
pass # Suppress HTTP server logs
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def login_browser(timeout: int = 120) -> bool:
|
|
129
|
+
"""Open browser for OAuth login. Returns True on success."""
|
|
130
|
+
import socket
|
|
131
|
+
|
|
132
|
+
# Find a free port
|
|
133
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
134
|
+
s.bind(("127.0.0.1", 0))
|
|
135
|
+
port = s.getsockname()[1]
|
|
136
|
+
|
|
137
|
+
# Single-use nonce the /cli-auth page echoes back, so we can tell our own
|
|
138
|
+
# callback apart from any other request that reaches this port.
|
|
139
|
+
state = secrets.token_urlsafe(24)
|
|
140
|
+
|
|
141
|
+
# Reset handler state
|
|
142
|
+
_CallbackHandler.token = None
|
|
143
|
+
_CallbackHandler.user_info = None
|
|
144
|
+
_CallbackHandler.expected_state = state
|
|
145
|
+
|
|
146
|
+
server = HTTPServer(("127.0.0.1", port), _CallbackHandler)
|
|
147
|
+
# Poll in short slices so a stray request can't hold us past the deadline.
|
|
148
|
+
server.timeout = 1
|
|
149
|
+
|
|
150
|
+
# The dashboard's /cli-auth page signs you in, then redirects to our
|
|
151
|
+
# loopback callback with the token. Derive nothing from the API URL: the
|
|
152
|
+
# app is a different host (app.kiteml.com vs api.kiteml.com).
|
|
153
|
+
login_url = f"{get_app_url().rstrip('/')}/cli-auth?redirect_port={port}&state={state}"
|
|
154
|
+
|
|
155
|
+
print(f"Opening browser for login...")
|
|
156
|
+
print(f"If the browser doesn't open, visit: {login_url}")
|
|
157
|
+
webbrowser.open(login_url)
|
|
158
|
+
|
|
159
|
+
# Wait for callback
|
|
160
|
+
deadline = time.time() + timeout
|
|
161
|
+
while time.time() < deadline and _CallbackHandler.token is None:
|
|
162
|
+
server.handle_request()
|
|
163
|
+
|
|
164
|
+
server.server_close()
|
|
165
|
+
|
|
166
|
+
if _CallbackHandler.token:
|
|
167
|
+
save_credentials(
|
|
168
|
+
id_token=_CallbackHandler.token,
|
|
169
|
+
user_info=_CallbackHandler.user_info,
|
|
170
|
+
)
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def login_headless() -> bool:
|
|
177
|
+
"""Headless login: prompt user to paste a token."""
|
|
178
|
+
print("Visit your Kite dashboard to generate an API token.")
|
|
179
|
+
print("Then paste it here:")
|
|
180
|
+
token = input("> ").strip()
|
|
181
|
+
if token:
|
|
182
|
+
save_credentials(id_token=token)
|
|
183
|
+
return True
|
|
184
|
+
return False
|
|
File without changes
|
|
File without changes
|