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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
@@ -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,3 @@
1
+ """Kite CLI and MCP server for the Kite robotics training platform."""
2
+
3
+ __version__ = "0.1.0"
@@ -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