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.
- onekey_cli-0.1.0/PKG-INFO +123 -0
- onekey_cli-0.1.0/README.md +98 -0
- onekey_cli-0.1.0/onekey_cli/__init__.py +517 -0
- onekey_cli-0.1.0/onekey_cli.egg-info/PKG-INFO +123 -0
- onekey_cli-0.1.0/onekey_cli.egg-info/SOURCES.txt +9 -0
- onekey_cli-0.1.0/onekey_cli.egg-info/dependency_links.txt +1 -0
- onekey_cli-0.1.0/onekey_cli.egg-info/entry_points.txt +2 -0
- onekey_cli-0.1.0/onekey_cli.egg-info/requires.txt +4 -0
- onekey_cli-0.1.0/onekey_cli.egg-info/top_level.txt +1 -0
- onekey_cli-0.1.0/pyproject.toml +45 -0
- onekey_cli-0.1.0/setup.cfg +4 -0
|
@@ -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
|
+
[](https://www.python.org/)
|
|
34
|
+
[](https://opensource.org/licenses/MIT)
|
|
35
|
+
[](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
|
+
[](https://www.python.org/)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](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
|
+
[](https://www.python.org/)
|
|
34
|
+
[](https://opensource.org/licenses/MIT)
|
|
35
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -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*"]
|