onekey-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,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: onekey-cli
3
+ Version: 0.1.0
4
+ Summary: Secure & unified API key management for developers
5
+ Author-email: Anikchand <anikchand461@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/anikchand461/onekey
8
+ Project-URL: Repository, https://github.com/anikchand461/onekey
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: typer>=0.12.0
22
+ Requires-Dist: rich>=13.0
23
+ Requires-Dist: requests>=2.30.0
24
+ Requires-Dist: click>=8.0
25
+
26
+ <div align="center">
27
+
28
+ # onekey
29
+
30
+ **Unified API Key Management for Developers**
31
+ Securely store, manage, rotate, and call **multiple AI/LLM API keys** (OpenAI, Anthropic, Groq, Gemini, and more) from one place — with zero plaintext leaks and unified interface.
32
+
33
+ [![Python](https://img.shields.io/badge/Python-3.8+-3776AB?style=flat&logo=python&logoColor=white)](https://www.python.org/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
+ [![CLI on PyPI](https://img.shields.io/pypi/v/onekey-cli?label=onekey-cli&logo=pypi)](https://pypi.org/project/onekey-cli/) <!-- update when published -->
36
+
37
+ </div>
38
+
39
+ ## The Problem Developers Face Every Day
40
+
41
+ Juggling **10–50+ API keys** across providers is painful:
42
+
43
+ - Scattered `.env` files everywhere → easy to commit by mistake to Git
44
+ - Plaintext keys on disk → security nightmare
45
+ - Different API signatures → constant code changes when switching providers
46
+ - No visibility into usage → surprise bills, no idea which key is burning tokens
47
+ - Hard to rotate or expire keys → manual, error-prone process
48
+
49
+ **onekey solves all of this** — one secure vault, one unified interface, beautiful CLI + web dashboard, **zero vendor lock-in**.
50
+
51
+ ## Key Features
52
+
53
+ - **AES-256 client-side encryption** — keys never touch the server in plaintext
54
+ - **Deployment** - Render
55
+ - **Unified proxy** — call any provider with the same format (`/proxy/u/<provider>/<slug>`)
56
+ - **CLI power tool** (`onekey`): add, list, delete, call, usage — with **Rich** beautiful tables, sparklines & panels
57
+ - **Automatic provider detection** (e.g. `sk-` → OpenAI)
58
+ - **Built-in usage tracking** — tokens used, latency, status codes, errors — per key/provider
59
+ - **Flexible auth** — JWT (username/password) + OAuth (GitHub / GitLab)
60
+ - **Normalized responses** — consistent output across OpenAI, Groq, Anthropic, Gemini ...
61
+ - **CLI for fast development** — install via `pipx install onekey-cli`, works everywhere (macOS/Linux/Windows)
62
+
63
+ ## Architecture
64
+
65
+ ```
66
+ onekey/
67
+ ├── backend/ # FastAPI server (auth, vault, proxy, usage)
68
+ ├── frontend/ # frontend on valinna js , HTML and css
69
+ ├── onekey_cli/ # Typer + Rich CLI (published as onekey-cli on PyPI)
70
+ ├── Dockerfile # Easy containerization
71
+ └── ... (pyproject.toml, requirements.txt, etc.)
72
+ ```
73
+
74
+ - Database: \*\*NeonDB — PostgreSQL support planned
75
+ - Encryption: **AES-256** at rest (client-side)
76
+ - Auth: **Argon2** password hashing + **JWT** (short-lived)
77
+ - Proxy: Normalizes requests/responses + retries + logging
78
+
79
+ ## Quick Start
80
+
81
+ ### 1. CLI (recommended for daily use)
82
+
83
+ ```bash
84
+ pipx install onekey-cli # install the pypy package
85
+
86
+ onekey login # OAuth or username/password
87
+ onekey add-key # Add your OpenAI / Groq / etc. key
88
+ onekey ls # Beautiful table of all keys
89
+ onekey call <unified_key> # Test call using unified key
90
+ onekey usage # Usage overview with sparklines
91
+ ```
92
+
93
+ ### 2. Full stack (fastAPI backend + interactive Vanilla JS frontend)
94
+
95
+ - hosted link : https://onekey.onrender.com/
96
+
97
+ Default: backend on `http://localhost:8000`, frontend proxies to it.
98
+
99
+ ## Security Highlights
100
+
101
+ - Keys encrypted **client-side** before ever hitting the database
102
+ - No plaintext in logs, memory, or disk
103
+ - Short-lived JWTs + secure OAuth flows
104
+ - Strict input validation (Pydantic)
105
+ - Security headers & CORS in FastAPI
106
+ - Designed with **zero-trust** principles
107
+
108
+ ## Who Is This For?
109
+
110
+ - ML/AI engineers juggling multiple LLM providers
111
+ - Indie hackers & solo devs tired of `.env` chaos
112
+ - Anyone who wants **usage visibility** in a easy way
113
+ - One who wants to store their API keys at a single place
114
+
115
+ ## License
116
+
117
+ [MIT License](LICENSE) — free to use, modify, distribute.
118
+
119
+ ---
120
+
121
+ Built with ❤️ by **[Anik Chand](https://github.com/anikchand461)** and **[Abhiraj Adhikary](https://github.com/abhirajadhikary06)**
122
+ **onekey** — because your keys deserve better than a `.env` file.
123
+
@@ -0,0 +1,98 @@
1
+ <div align="center">
2
+
3
+ # onekey
4
+
5
+ **Unified API Key Management for Developers**
6
+ Securely store, manage, rotate, and call **multiple AI/LLM API keys** (OpenAI, Anthropic, Groq, Gemini, and more) from one place — with zero plaintext leaks and unified interface.
7
+
8
+ [![Python](https://img.shields.io/badge/Python-3.8+-3776AB?style=flat&logo=python&logoColor=white)](https://www.python.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+ [![CLI on PyPI](https://img.shields.io/pypi/v/onekey-cli?label=onekey-cli&logo=pypi)](https://pypi.org/project/onekey-cli/) <!-- update when published -->
11
+
12
+ </div>
13
+
14
+ ## The Problem Developers Face Every Day
15
+
16
+ Juggling **10–50+ API keys** across providers is painful:
17
+
18
+ - Scattered `.env` files everywhere → easy to commit by mistake to Git
19
+ - Plaintext keys on disk → security nightmare
20
+ - Different API signatures → constant code changes when switching providers
21
+ - No visibility into usage → surprise bills, no idea which key is burning tokens
22
+ - Hard to rotate or expire keys → manual, error-prone process
23
+
24
+ **onekey solves all of this** — one secure vault, one unified interface, beautiful CLI + web dashboard, **zero vendor lock-in**.
25
+
26
+ ## Key Features
27
+
28
+ - **AES-256 client-side encryption** — keys never touch the server in plaintext
29
+ - **Deployment** - Render
30
+ - **Unified proxy** — call any provider with the same format (`/proxy/u/<provider>/<slug>`)
31
+ - **CLI power tool** (`onekey`): add, list, delete, call, usage — with **Rich** beautiful tables, sparklines & panels
32
+ - **Automatic provider detection** (e.g. `sk-` → OpenAI)
33
+ - **Built-in usage tracking** — tokens used, latency, status codes, errors — per key/provider
34
+ - **Flexible auth** — JWT (username/password) + OAuth (GitHub / GitLab)
35
+ - **Normalized responses** — consistent output across OpenAI, Groq, Anthropic, Gemini ...
36
+ - **CLI for fast development** — install via `pipx install onekey-cli`, works everywhere (macOS/Linux/Windows)
37
+
38
+ ## Architecture
39
+
40
+ ```
41
+ onekey/
42
+ ├── backend/ # FastAPI server (auth, vault, proxy, usage)
43
+ ├── frontend/ # frontend on valinna js , HTML and css
44
+ ├── onekey_cli/ # Typer + Rich CLI (published as onekey-cli on PyPI)
45
+ ├── Dockerfile # Easy containerization
46
+ └── ... (pyproject.toml, requirements.txt, etc.)
47
+ ```
48
+
49
+ - Database: \*\*NeonDB — PostgreSQL support planned
50
+ - Encryption: **AES-256** at rest (client-side)
51
+ - Auth: **Argon2** password hashing + **JWT** (short-lived)
52
+ - Proxy: Normalizes requests/responses + retries + logging
53
+
54
+ ## Quick Start
55
+
56
+ ### 1. CLI (recommended for daily use)
57
+
58
+ ```bash
59
+ pipx install onekey-cli # install the pypy package
60
+
61
+ onekey login # OAuth or username/password
62
+ onekey add-key # Add your OpenAI / Groq / etc. key
63
+ onekey ls # Beautiful table of all keys
64
+ onekey call <unified_key> # Test call using unified key
65
+ onekey usage # Usage overview with sparklines
66
+ ```
67
+
68
+ ### 2. Full stack (fastAPI backend + interactive Vanilla JS frontend)
69
+
70
+ - hosted link : https://onekey.onrender.com/
71
+
72
+ Default: backend on `http://localhost:8000`, frontend proxies to it.
73
+
74
+ ## Security Highlights
75
+
76
+ - Keys encrypted **client-side** before ever hitting the database
77
+ - No plaintext in logs, memory, or disk
78
+ - Short-lived JWTs + secure OAuth flows
79
+ - Strict input validation (Pydantic)
80
+ - Security headers & CORS in FastAPI
81
+ - Designed with **zero-trust** principles
82
+
83
+ ## Who Is This For?
84
+
85
+ - ML/AI engineers juggling multiple LLM providers
86
+ - Indie hackers & solo devs tired of `.env` chaos
87
+ - Anyone who wants **usage visibility** in a easy way
88
+ - One who wants to store their API keys at a single place
89
+
90
+ ## License
91
+
92
+ [MIT License](LICENSE) — free to use, modify, distribute.
93
+
94
+ ---
95
+
96
+ Built with ❤️ by **[Anik Chand](https://github.com/anikchand461)** and **[Abhiraj Adhikary](https://github.com/abhirajadhikary06)**
97
+ **onekey** — because your keys deserve better than a `.env` file.
98
+
@@ -0,0 +1,517 @@
1
+ # cli.py
2
+ import typer
3
+ import requests
4
+ import json
5
+ from typing import Optional, List
6
+ from pathlib import Path
7
+ from datetime import datetime
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ from rich.panel import Panel
11
+ from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn
12
+ from rich import print as rprint
13
+ from rich.text import Text
14
+ from rich.box import ROUNDED
15
+ import click
16
+
17
+ console = Console()
18
+
19
+ app = typer.Typer(
20
+ name="onekey",
21
+ help="onekey CLI - Secure & unified API key management",
22
+ add_completion=False,
23
+ no_args_is_help=True,
24
+ )
25
+
26
+ __version__ = "0.1.0"
27
+
28
+ # Config
29
+ BASE_URL = "https://onekey-8pr2.onrender.com" # production URL later
30
+ CONFIG_FILE = Path.home() / ".onekey" / "config.json"
31
+ CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
32
+
33
+ def save_token(token: str):
34
+ CONFIG_FILE.write_text(json.dumps({"access_token": token}))
35
+
36
+ def load_token() -> Optional[str]:
37
+ if CONFIG_FILE.exists():
38
+ return json.loads(CONFIG_FILE.read_text()).get("access_token")
39
+ return None
40
+
41
+ def get_headers() -> dict:
42
+ token = load_token()
43
+ if not token:
44
+ console.print("[bold red]✗ Not logged in. Run:[/bold red] onekey login")
45
+ raise typer.Exit(1)
46
+ return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
47
+
48
+ # ────────────────────────────────────────────────
49
+ # Auth Commands
50
+ # ────────────────────────────────────────────────
51
+
52
+ @app.command()
53
+ def register():
54
+ """Register a new account"""
55
+ username = typer.prompt("Username")
56
+ password = typer.prompt("Password", hide_input=True, confirmation_prompt=True)
57
+ email = typer.prompt("Email (optional)", default="", show_default=False) or None
58
+
59
+ payload = {"username": username, "password": password}
60
+ if email:
61
+ payload["email"] = email
62
+
63
+ with console.status("[cyan]Creating account..."):
64
+ try:
65
+ r = requests.post(f"{BASE_URL}/auth/register", json=payload)
66
+ r.raise_for_status()
67
+ console.print(Panel(
68
+ "[bold green]✓ Account created successfully![/bold green]\nNow login with onekey login",
69
+ title="Success",
70
+ border_style="green",
71
+ expand=False
72
+ ))
73
+ except requests.HTTPError as e:
74
+ console.print(f"[red]✗ Error: {e.response.json().get('detail', 'Unknown error')}[/red]")
75
+ raise typer.Exit(1)
76
+
77
+ @app.command()
78
+ def login():
79
+ """Login to your account (JWT or GitHub)"""
80
+
81
+ console.print("\n[bold cyan]Choose Authentication Method:[/bold cyan]\n")
82
+
83
+ # Display options with logos/icons
84
+ console.print(" [bold]1.[/bold] JWT Authentication (Username & Password)")
85
+ console.print(" [bold]2.[/bold] GitHub OAuth")
86
+ console.print(" [bold]3.[/bold] GitLab OAuth")
87
+ console.print(" [bold]4.[/bold] Bitbucket OAuth\n")
88
+
89
+ choice = typer.prompt(
90
+ "Enter your choice",
91
+ type=click.Choice(["1", "2", "3", "4"]),
92
+ default="1"
93
+ )
94
+
95
+ if choice == "1":
96
+ auth_choice = "jwt"
97
+ elif choice == "2":
98
+ auth_choice = "github"
99
+ elif choice == "3":
100
+ auth_choice = "gitlab"
101
+ else:
102
+ auth_choice = "bitbucket"
103
+
104
+ if auth_choice == "jwt":
105
+ console.print("\n[bold cyan] JWT Login[/bold cyan]")
106
+ username = typer.prompt("Username")
107
+ password = typer.prompt("Password", hide_input=True)
108
+
109
+ payload = f"username={username}&password={password}"
110
+ with console.status("[cyan]Logging in..."):
111
+ try:
112
+ r = requests.post(
113
+ f"{BASE_URL}/auth/login",
114
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
115
+ data=payload
116
+ )
117
+ r.raise_for_status()
118
+ token = r.json()["access_token"]
119
+ save_token(token)
120
+ console.print(Panel(
121
+ "[bold green]✓ JWT Login successful![/bold green]\nToken saved securely.",
122
+ title="Success",
123
+ border_style="green",
124
+ expand=False
125
+ ))
126
+ except requests.HTTPError as e:
127
+ console.print(f"[red]✗ Login failed: {e.response.json().get('detail', 'Unknown error')}[/red]")
128
+ raise typer.Exit(1)
129
+
130
+ elif auth_choice == "github":
131
+ import webbrowser
132
+ import time
133
+
134
+ console.print(Panel(
135
+ "[bold cyan]:github: GitHub Login Flow[/bold cyan]\n\n"
136
+ "[yellow]Step 1:[/yellow] Opening GitHub authorization in your browser...\n"
137
+ "[yellow]Step 2:[/yellow] After you authorize, your JWT will be shown in the browser\n"
138
+ "[yellow]Step 3:[/yellow] Copy it and paste back here to finish CLI login",
139
+ title="GitHub OAuth",
140
+ border_style="cyan",
141
+ expand=False
142
+ ))
143
+
144
+ github_url = f"{BASE_URL}/auth/github/login?state=cli"
145
+ console.print(f"\n[blue]Opening: {github_url}[/blue]\n")
146
+
147
+ webbrowser.open(github_url)
148
+
149
+ console.print("\n[bold yellow]After authorizing, copy the JWT shown in the browser and paste it below.[/bold yellow]\n")
150
+ pasted_token = typer.prompt("Paste JWT from browser", hide_input=True)
151
+
152
+ if not pasted_token.strip():
153
+ console.print("[red]✗ No token provided. Aborting.[/red]")
154
+ raise typer.Exit(1)
155
+
156
+ save_token(pasted_token.strip())
157
+ console.print(Panel(
158
+ "[bold green]✓ GitHub login successful via CLI![/bold green]\nToken saved securely.",
159
+ title="Success",
160
+ border_style="green",
161
+ expand=False
162
+ ))
163
+
164
+ elif auth_choice == "gitlab":
165
+ import webbrowser
166
+ import time
167
+
168
+ console.print(Panel(
169
+ "[bold cyan]:gitlab: GitLab Login Flow[/bold cyan]\n\n"
170
+ "[yellow]Step 1:[/yellow] Opening GitLab authorization in your browser...\n"
171
+ "[yellow]Step 2:[/yellow] After you authorize, your JWT will be shown in the browser\n"
172
+ "[yellow]Step 3:[/yellow] Copy it and paste back here to finish CLI login",
173
+ title="GitLab OAuth",
174
+ border_style="cyan",
175
+ expand=False
176
+ ))
177
+
178
+ gitlab_url = f"{BASE_URL}/auth/gitlab/login?state=cli"
179
+ console.print(f"\n[blue]Opening: {gitlab_url}[/blue]\n")
180
+
181
+ webbrowser.open(gitlab_url)
182
+
183
+ console.print("\n[bold yellow]After authorizing, copy the JWT shown in the browser and paste it below.[/bold yellow]\n")
184
+ pasted_token = typer.prompt("Paste JWT from browser", hide_input=True)
185
+
186
+ if not pasted_token.strip():
187
+ console.print("[red]✗ No token provided. Aborting.[/red]")
188
+ raise typer.Exit(1)
189
+
190
+ save_token(pasted_token.strip())
191
+ console.print(Panel(
192
+ "[bold green]✓ GitLab login successful via CLI![/bold green]\nToken saved securely.",
193
+ title="Success",
194
+ border_style="green",
195
+ expand=False
196
+ ))
197
+
198
+ elif auth_choice == "bitbucket":
199
+
200
+ console.print(Panel(
201
+ "[bold cyan]:bitbucket: Bitbucket Login Flow[/bold cyan]\n\n"
202
+ "[yellow]Step 1:[/yellow] Opening Bitbucket authorization in your browser...\n"
203
+ "[yellow]Step 2:[/yellow] After you authorize, your JWT will be shown in the browser\n"
204
+ "[yellow]Step 3:[/yellow] Copy it and paste back here to finish CLI login",
205
+ title="Bitbucket OAuth",
206
+ border_style="cyan",
207
+ expand=False
208
+ ))
209
+
210
+ bitbucket_url = f"{BASE_URL}/auth/bitbucket/login?state=cli"
211
+ console.print(f"\n[blue]Opening: {bitbucket_url}[/blue]\n")
212
+
213
+ webbrowser.open(bitbucket_url)
214
+
215
+ console.print("\n[bold yellow]After authorizing, copy the JWT shown in the browser and paste it below.[/bold yellow]\n")
216
+ pasted_token = typer.prompt("Paste JWT from browser", hide_input=True)
217
+
218
+ if not pasted_token.strip():
219
+ console.print("[red]✗ No token provided. Aborting.[/red]")
220
+ raise typer.Exit(1)
221
+
222
+ save_token(pasted_token.strip())
223
+ console.print(Panel(
224
+ "[bold green]✓ Bitbucket login successful via CLI![/bold green]\nToken saved securely.",
225
+ title="Success",
226
+ border_style="green",
227
+ expand=False
228
+ ))
229
+
230
+ @app.command()
231
+ def logout():
232
+ """Logout and clear saved token"""
233
+ if CONFIG_FILE.exists():
234
+ CONFIG_FILE.unlink()
235
+ console.print("[green]✓ Logged out successfully.[/green]")
236
+ else:
237
+ console.print("[yellow]No saved token found.[/yellow]")
238
+
239
+ # ────────────────────────────────────────────────
240
+ # API Key Management – Beautiful Table
241
+ # ────────────────────────────────────────────────
242
+
243
+ @app.command()
244
+ def add_key():
245
+ """Add a new API key (provider auto-detected)"""
246
+ name = typer.prompt("Name (e.g. production-groq)")
247
+ key = typer.prompt("API Key")
248
+ expires = typer.prompt("Expiration date (YYYY-MM-DD) or press Enter for Never", default="", show_default=False)
249
+
250
+ payload = {"name": name, "key": key}
251
+ if expires.strip():
252
+ payload["expires_at"] = expires
253
+
254
+ with console.status("[cyan]Adding key..."):
255
+ try:
256
+ r = requests.post(f"{BASE_URL}/keys", json=payload, headers=get_headers())
257
+ r.raise_for_status()
258
+ data = r.json()
259
+
260
+ console.print(Panel.fit(
261
+ f"[bold green]✓ Key added successfully![/bold green]\n\n"
262
+ f"Provider: [cyan]{data['provider']}[/cyan]\n"
263
+ f"Name: [bold]{data['name']}[/bold]\n"
264
+ f"Unified API Key: [bold cyan]{data['unified_api_key']}[/bold cyan]\n"
265
+ f"Endpoint: [blue]{data['unified_endpoint']}[/blue]\n"
266
+ f"Expires: [yellow]{data['expires_at'] or 'Never'}[/yellow]",
267
+ title="Key Details",
268
+ border_style="green",
269
+ padding=(1, 2)
270
+ ))
271
+ except requests.HTTPError as e:
272
+ console.print(f"[red]✗ {e.response.json().get('detail', 'Failed to add key')}[/red]")
273
+ raise typer.Exit(1)
274
+
275
+ @app.command(name="ls")
276
+ def list_keys():
277
+ """List all your API keys with serial numbers (beautiful table)"""
278
+ try:
279
+ r = requests.get(f"{BASE_URL}/keys", headers=get_headers())
280
+ r.raise_for_status()
281
+ keys = r.json()
282
+
283
+ if not keys:
284
+ console.print(Panel("[yellow]No keys stored yet. Add one with 'onekey add-key'[/yellow]", border_style="yellow"))
285
+ return
286
+
287
+ table = Table(title="Your onekey Vault", show_header=True, header_style="bold magenta", box=ROUNDED)
288
+ table.add_column("Sl. No.", style="cyan bold", justify="center")
289
+ table.add_column("Name", style="bold white")
290
+ table.add_column("Provider", style="green")
291
+ table.add_column("Unified API Key", style="blue")
292
+ table.add_column("Expires", style="yellow", justify="right")
293
+
294
+ for idx, k in enumerate(keys, 1):
295
+ expires = k.get('expires_at') or "Never"
296
+ table.add_row(
297
+ f"[bold cyan]{idx}[/bold cyan]",
298
+ k['name'],
299
+ k['provider'],
300
+ k['unified_api_key'][:30] + "..." if len(k['unified_api_key']) > 30 else k['unified_api_key'],
301
+ expires
302
+ )
303
+
304
+ console.print(table)
305
+ console.print(f"\n[italic dim]Total keys: {len(keys)}[/italic dim]")
306
+
307
+ except requests.HTTPError as e:
308
+ console.print(f"[red]✗ Failed to load keys: {e.response.json().get('detail', 'Unknown error')}[/red]")
309
+
310
+ @app.command()
311
+ def delete(sl_no: int = typer.Argument(..., help="Serial number from 'onekey ls'")):
312
+ """Delete an API key using its serial number"""
313
+ try:
314
+ r = requests.get(f"{BASE_URL}/keys", headers=get_headers())
315
+ r.raise_for_status()
316
+ keys = r.json()
317
+ except requests.HTTPError:
318
+ console.print("[red]✗ Could not fetch keys[/red]")
319
+ raise typer.Exit(1)
320
+
321
+ if not keys:
322
+ console.print("[yellow]No keys to delete[/yellow]")
323
+ raise typer.Exit()
324
+
325
+ if sl_no < 1 or sl_no > len(keys):
326
+ console.print(f"[red]Invalid serial number. Valid range: 1–{len(keys)}[/red]")
327
+ raise typer.Exit(1)
328
+
329
+ key = keys[sl_no - 1]
330
+ key_id = key["id"]
331
+ key_name = key["name"]
332
+ key_provider = key["provider"]
333
+
334
+ console.print(Panel(
335
+ f"[bold yellow]Delete key:[/bold yellow] {key_name} ({key_provider})\n"
336
+ f"Sl. No.: [cyan]{sl_no}[/cyan]",
337
+ title="Confirmation",
338
+ border_style="yellow"
339
+ ))
340
+
341
+ if not typer.confirm("Are you sure?"):
342
+ console.print("[green]Cancelled.[/green]")
343
+ raise typer.Exit()
344
+
345
+ with console.status("[cyan]Deleting..."):
346
+ try:
347
+ r = requests.delete(f"{BASE_URL}/keys/{key_id}", headers=get_headers())
348
+ r.raise_for_status()
349
+ console.print(f"[green]✓ Key '{key_name}' (Sl. No. {sl_no}) deleted successfully.[/green]")
350
+ except requests.HTTPError as e:
351
+ console.print(f"[red]✗ Delete failed: {e.response.json().get('detail', 'Unknown error')}[/red]")
352
+
353
+ # ────────────────────────────────────────────────
354
+ # Beautiful Usage Curve
355
+ # ────────────────────────────────────────────────
356
+
357
+ def sparkline(values: List[int], width: int = 50, height: int = 8) -> str:
358
+ """Generate beautiful vertical sparkline with rich colors"""
359
+ if not values:
360
+ return "─" * width
361
+ max_v = max(values) or 1
362
+ min_v = min(values)
363
+ range_v = max_v - min_v or 1
364
+ scaled = [int((v - min_v) / range_v * (height - 1)) for v in values]
365
+ lines = []
366
+ for y in range(height - 1, -1, -1):
367
+ line = ""
368
+ for s in scaled:
369
+ if s > y:
370
+ line += "█"
371
+ elif s == y:
372
+ line += "▉"
373
+ else:
374
+ line += " "
375
+ lines.append(line)
376
+ return "\n".join(lines)
377
+
378
+ @app.command()
379
+ def usage(sl_no: Optional[int] = typer.Argument(None, help="Serial number from 'onekey ls' (optional)")):
380
+ """Show beautiful usage curve for all keys or specific key"""
381
+ try:
382
+ if sl_no is None:
383
+ # Total usage
384
+ r = requests.get(f"{BASE_URL}/usage", headers=get_headers())
385
+ title = "Total Usage Across All Keys"
386
+ else:
387
+ # Get key list to map sl_no → id
388
+ keys_r = requests.get(f"{BASE_URL}/keys", headers=get_headers())
389
+ keys_r.raise_for_status()
390
+ keys = keys_r.json()
391
+ if sl_no < 1 or sl_no > len(keys):
392
+ console.print(f"[red]Invalid serial number. Run 'onekey ls' first.[/red]")
393
+ raise typer.Exit(1)
394
+ key_id = keys[sl_no - 1]["id"]
395
+ key_name = keys[sl_no - 1]["name"]
396
+ r = requests.get(f"{BASE_URL}/usage/{key_id}", headers=get_headers())
397
+ title = f"Usage for {key_name} (Sl. No. {sl_no})"
398
+
399
+ r.raise_for_status()
400
+ data = r.json()
401
+ logs = data.get("logs", []) if sl_no else data
402
+
403
+ if not logs:
404
+ console.print(Panel("[yellow]No usage recorded yet.[/yellow]", title=title, border_style="yellow"))
405
+ return
406
+
407
+ # Sort logs by time
408
+ logs.sort(key=lambda x: x["created_at"])
409
+ tokens = [log["total_tokens"] for log in logs]
410
+ times = [datetime.fromisoformat(log["created_at"].replace("Z", "+00:00")) for log in logs]
411
+
412
+ # Sparkline + Stats Panel
413
+ spark = sparkline(tokens)
414
+ total = sum(tokens)
415
+ max_single = max(tokens) if tokens else 0
416
+ calls = len(logs)
417
+ time_range = f"{times[0].strftime('%b %d %Y')} → {times[-1].strftime('%b %d %Y')}"
418
+
419
+ stats_text = Text.assemble(
420
+ ("Total tokens: ", "bold green"), (f"{total:,}", "bold white"),
421
+ ("\nHighest call: ", "bold green"), (f"{max_single:,}", "bold white"),
422
+ ("\nTotal calls: ", "bold green"), (f"{calls}", "bold white"),
423
+ ("\nTime span: ", "bold green"), (time_range, "bold white")
424
+ )
425
+
426
+ console.print(Panel(
427
+ f"[bold cyan]{title}[/bold cyan]\n\n"
428
+ f"{spark}\n\n"
429
+ f"{stats_text}",
430
+ title="Usage Curve & Stats",
431
+ border_style="bright_blue",
432
+ expand=False,
433
+ padding=(1, 2)
434
+ ))
435
+
436
+ except requests.HTTPError as e:
437
+ console.print(f"[red]✗ Failed to load usage: {e.response.json().get('detail', 'Unknown error')}[/red]")
438
+
439
+ # ────────────────────────────────────────────────
440
+ # Quick Proxy Call
441
+ # ────────────────────────────────────────────────
442
+
443
+ # cli.py (updated call command only - replace your existing call function)
444
+
445
+ @app.command()
446
+ def call(
447
+ unified_key: str = typer.Argument(..., help="Unified API key (e.g. apikey-gemini-cbhgemini)"),
448
+ model: str = typer.Option(..., prompt="Model name (e.g. llama-3.3-70b-versatile for Groq, gemini-1.5-flash for Gemini)", help="Model to use"),
449
+ message: str = typer.Option(..., prompt=True, help="Your prompt/message")
450
+ ):
451
+ """Quickly call any API using unified key and show full proper JSON response"""
452
+ # Parse unified_key
453
+ parts = unified_key.split('-')
454
+ if len(parts) < 3 or parts[0] != 'apikey':
455
+ console.print("[red]Invalid unified key format. Expected: apikey-provider-name[/red]")
456
+ raise typer.Exit(1)
457
+
458
+ provider = parts[1]
459
+ name_slug = '-'.join(parts[2:])
460
+
461
+ payload = {
462
+ "model": model,
463
+ "messages": [{"role": "user", "content": message}]
464
+ }
465
+
466
+ with console.status(f"[cyan]Calling {provider.upper()} ({model})..."):
467
+ try:
468
+ r = requests.post(
469
+ f"{BASE_URL}/proxy/u/{provider}/{name_slug}",
470
+ json=payload,
471
+ headers={
472
+ "Content-Type": "application/json",
473
+ "Authorization": f"Bearer {unified_key}"
474
+ }
475
+ )
476
+ r.raise_for_status()
477
+ response_data = r.json()
478
+
479
+ # Full JSON string
480
+ json_str = json.dumps(response_data, indent=2)
481
+
482
+ # Simple colorization (keys cyan, values green)
483
+ colored_json = Text()
484
+ for line in json_str.splitlines():
485
+ if ':' in line and not line.strip().startswith('}'):
486
+ key, value = line.split(":", 1)
487
+ colored_json.append(Text(key, style="cyan"))
488
+ colored_json.append(Text(":", style="white"))
489
+ colored_json.append(Text(value, style="green"))
490
+ else:
491
+ colored_json.append(Text(line, style="white"))
492
+ colored_json.append("\n")
493
+
494
+ console.print(Panel(
495
+ colored_json,
496
+ title=f"Full API Response • {provider.upper()} • {model}",
497
+ subtitle=f"Prompt: {message[:80]}{'...' if len(message) > 80 else ''}",
498
+ border_style="bright_green",
499
+ expand=True,
500
+ padding=(1, 2)
501
+ ))
502
+
503
+ except requests.HTTPError as e:
504
+ console.print(f"[red]✗ API call failed: {e.response.status_code}[/red]")
505
+ try:
506
+ console.print(Panel(
507
+ json.dumps(e.response.json(), indent=2),
508
+ title="Error Response",
509
+ border_style="red",
510
+ expand=True
511
+ ))
512
+ except:
513
+ console.print(e.response.text)
514
+
515
+
516
+ if __name__ == "__main__":
517
+ app()
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: onekey-cli
3
+ Version: 0.1.0
4
+ Summary: Secure & unified API key management for developers
5
+ Author-email: Anikchand <anikchand461@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/anikchand461/onekey
8
+ Project-URL: Repository, https://github.com/anikchand461/onekey
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: typer>=0.12.0
22
+ Requires-Dist: rich>=13.0
23
+ Requires-Dist: requests>=2.30.0
24
+ Requires-Dist: click>=8.0
25
+
26
+ <div align="center">
27
+
28
+ # onekey
29
+
30
+ **Unified API Key Management for Developers**
31
+ Securely store, manage, rotate, and call **multiple AI/LLM API keys** (OpenAI, Anthropic, Groq, Gemini, and more) from one place — with zero plaintext leaks and unified interface.
32
+
33
+ [![Python](https://img.shields.io/badge/Python-3.8+-3776AB?style=flat&logo=python&logoColor=white)](https://www.python.org/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
+ [![CLI on PyPI](https://img.shields.io/pypi/v/onekey-cli?label=onekey-cli&logo=pypi)](https://pypi.org/project/onekey-cli/) <!-- update when published -->
36
+
37
+ </div>
38
+
39
+ ## The Problem Developers Face Every Day
40
+
41
+ Juggling **10–50+ API keys** across providers is painful:
42
+
43
+ - Scattered `.env` files everywhere → easy to commit by mistake to Git
44
+ - Plaintext keys on disk → security nightmare
45
+ - Different API signatures → constant code changes when switching providers
46
+ - No visibility into usage → surprise bills, no idea which key is burning tokens
47
+ - Hard to rotate or expire keys → manual, error-prone process
48
+
49
+ **onekey solves all of this** — one secure vault, one unified interface, beautiful CLI + web dashboard, **zero vendor lock-in**.
50
+
51
+ ## Key Features
52
+
53
+ - **AES-256 client-side encryption** — keys never touch the server in plaintext
54
+ - **Deployment** - Render
55
+ - **Unified proxy** — call any provider with the same format (`/proxy/u/<provider>/<slug>`)
56
+ - **CLI power tool** (`onekey`): add, list, delete, call, usage — with **Rich** beautiful tables, sparklines & panels
57
+ - **Automatic provider detection** (e.g. `sk-` → OpenAI)
58
+ - **Built-in usage tracking** — tokens used, latency, status codes, errors — per key/provider
59
+ - **Flexible auth** — JWT (username/password) + OAuth (GitHub / GitLab)
60
+ - **Normalized responses** — consistent output across OpenAI, Groq, Anthropic, Gemini ...
61
+ - **CLI for fast development** — install via `pipx install onekey-cli`, works everywhere (macOS/Linux/Windows)
62
+
63
+ ## Architecture
64
+
65
+ ```
66
+ onekey/
67
+ ├── backend/ # FastAPI server (auth, vault, proxy, usage)
68
+ ├── frontend/ # frontend on valinna js , HTML and css
69
+ ├── onekey_cli/ # Typer + Rich CLI (published as onekey-cli on PyPI)
70
+ ├── Dockerfile # Easy containerization
71
+ └── ... (pyproject.toml, requirements.txt, etc.)
72
+ ```
73
+
74
+ - Database: \*\*NeonDB — PostgreSQL support planned
75
+ - Encryption: **AES-256** at rest (client-side)
76
+ - Auth: **Argon2** password hashing + **JWT** (short-lived)
77
+ - Proxy: Normalizes requests/responses + retries + logging
78
+
79
+ ## Quick Start
80
+
81
+ ### 1. CLI (recommended for daily use)
82
+
83
+ ```bash
84
+ pipx install onekey-cli # install the pypy package
85
+
86
+ onekey login # OAuth or username/password
87
+ onekey add-key # Add your OpenAI / Groq / etc. key
88
+ onekey ls # Beautiful table of all keys
89
+ onekey call <unified_key> # Test call using unified key
90
+ onekey usage # Usage overview with sparklines
91
+ ```
92
+
93
+ ### 2. Full stack (fastAPI backend + interactive Vanilla JS frontend)
94
+
95
+ - hosted link : https://onekey.onrender.com/
96
+
97
+ Default: backend on `http://localhost:8000`, frontend proxies to it.
98
+
99
+ ## Security Highlights
100
+
101
+ - Keys encrypted **client-side** before ever hitting the database
102
+ - No plaintext in logs, memory, or disk
103
+ - Short-lived JWTs + secure OAuth flows
104
+ - Strict input validation (Pydantic)
105
+ - Security headers & CORS in FastAPI
106
+ - Designed with **zero-trust** principles
107
+
108
+ ## Who Is This For?
109
+
110
+ - ML/AI engineers juggling multiple LLM providers
111
+ - Indie hackers & solo devs tired of `.env` chaos
112
+ - Anyone who wants **usage visibility** in a easy way
113
+ - One who wants to store their API keys at a single place
114
+
115
+ ## License
116
+
117
+ [MIT License](LICENSE) — free to use, modify, distribute.
118
+
119
+ ---
120
+
121
+ Built with ❤️ by **[Anik Chand](https://github.com/anikchand461)** and **[Abhiraj Adhikary](https://github.com/abhirajadhikary06)**
122
+ **onekey** — because your keys deserve better than a `.env` file.
123
+
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ onekey_cli/__init__.py
4
+ onekey_cli.egg-info/PKG-INFO
5
+ onekey_cli.egg-info/SOURCES.txt
6
+ onekey_cli.egg-info/dependency_links.txt
7
+ onekey_cli.egg-info/entry_points.txt
8
+ onekey_cli.egg-info/requires.txt
9
+ onekey_cli.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ onekey = onekey_cli:app
@@ -0,0 +1,4 @@
1
+ typer>=0.12.0
2
+ rich>=13.0
3
+ requests>=2.30.0
4
+ click>=8.0
@@ -0,0 +1 @@
1
+ onekey_cli
@@ -0,0 +1,45 @@
1
+ # pyproject.toml
2
+ [build-system]
3
+ requires = ["setuptools>=61.0", "wheel"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ [project]
7
+ name = "onekey-cli"
8
+ version = "0.1.0"
9
+ description = "Secure & unified API key management for developers"
10
+ readme = "README.md"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Anikchand", email = "anikchand461@gmail.com" }
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ ]
27
+ dependencies = [
28
+ "typer>=0.12.0",
29
+ "rich>=13.0",
30
+ "requests>=2.30.0",
31
+ "click>=8.0"
32
+ ]
33
+ requires-python = ">=3.8"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/anikchand461/onekey"
37
+ Repository = "https://github.com/anikchand461/onekey"
38
+
39
+ [project.scripts]
40
+ onekey = "onekey_cli:app"
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["."]
44
+ include = ["onekey_cli"]
45
+ exclude = ["backend*", "frontend*", "release*", "tests*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+