klaude-code 1.2.6__py3-none-any.whl → 1.2.8__py3-none-any.whl
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.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/main.py +63 -0
- klaude_code/command/status_cmd.py +13 -5
- klaude_code/config/list_model.py +53 -0
- klaude_code/core/prompt.py +10 -14
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +3 -1
- klaude_code/core/reminders.py +14 -5
- klaude_code/core/task.py +1 -4
- klaude_code/core/tool/truncation.py +4 -0
- klaude_code/core/turn.py +66 -41
- klaude_code/llm/__init__.py +2 -0
- klaude_code/llm/anthropic/client.py +2 -2
- klaude_code/llm/anthropic/input.py +25 -10
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +129 -0
- klaude_code/llm/openai_compatible/client.py +2 -2
- klaude_code/llm/responses/client.py +181 -163
- klaude_code/llm/usage.py +3 -0
- klaude_code/protocol/events.py +1 -0
- klaude_code/protocol/llm_param.py +3 -1
- klaude_code/protocol/model.py +2 -3
- klaude_code/protocol/sub_agent.py +2 -1
- klaude_code/session/export.py +9 -14
- klaude_code/session/session.py +5 -0
- klaude_code/session/templates/export_session.html +5 -0
- klaude_code/ui/modes/repl/completers.py +41 -8
- klaude_code/ui/modes/repl/event_handler.py +15 -23
- klaude_code/ui/modes/repl/renderer.py +2 -0
- klaude_code/ui/renderers/developer.py +9 -8
- klaude_code/ui/renderers/metadata.py +9 -5
- klaude_code/ui/renderers/user_input.py +23 -10
- klaude_code/ui/rich/theme.py +2 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.2.8.dist-info}/METADATA +1 -1
- {klaude_code-1.2.6.dist-info → klaude_code-1.2.8.dist-info}/RECORD +43 -34
- /klaude_code/core/prompts/{prompt-codex.md → prompt-codex-gpt-5-1.md} +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.2.8.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.2.8.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Authentication module.
|
|
2
|
+
|
|
3
|
+
Currently includes Codex OAuth helpers in ``klaude_code.auth.codex``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from klaude_code.auth.codex import (
|
|
7
|
+
CodexAuthError,
|
|
8
|
+
CodexAuthState,
|
|
9
|
+
CodexNotLoggedInError,
|
|
10
|
+
CodexOAuth,
|
|
11
|
+
CodexOAuthError,
|
|
12
|
+
CodexTokenExpiredError,
|
|
13
|
+
CodexTokenManager,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CodexAuthError",
|
|
18
|
+
"CodexAuthState",
|
|
19
|
+
"CodexNotLoggedInError",
|
|
20
|
+
"CodexOAuth",
|
|
21
|
+
"CodexOAuthError",
|
|
22
|
+
"CodexTokenExpiredError",
|
|
23
|
+
"CodexTokenManager",
|
|
24
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Codex authentication helpers."""
|
|
2
|
+
|
|
3
|
+
from klaude_code.auth.codex.exceptions import (
|
|
4
|
+
CodexAuthError,
|
|
5
|
+
CodexNotLoggedInError,
|
|
6
|
+
CodexOAuthError,
|
|
7
|
+
CodexTokenExpiredError,
|
|
8
|
+
)
|
|
9
|
+
from klaude_code.auth.codex.oauth import CodexOAuth
|
|
10
|
+
from klaude_code.auth.codex.token_manager import CodexAuthState, CodexTokenManager
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CodexAuthError",
|
|
14
|
+
"CodexAuthState",
|
|
15
|
+
"CodexNotLoggedInError",
|
|
16
|
+
"CodexOAuth",
|
|
17
|
+
"CodexOAuthError",
|
|
18
|
+
"CodexTokenExpiredError",
|
|
19
|
+
"CodexTokenManager",
|
|
20
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Exceptions for Codex authentication."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CodexAuthError(Exception):
|
|
5
|
+
"""Base exception for Codex authentication errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CodexNotLoggedInError(CodexAuthError):
|
|
9
|
+
"""User has not logged in to Codex."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CodexTokenExpiredError(CodexAuthError):
|
|
13
|
+
"""Token expired and refresh failed."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CodexOAuthError(CodexAuthError):
|
|
17
|
+
"""OAuth flow failed."""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""JWT parsing utilities for Codex authentication."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def decode_jwt_payload(token: str) -> dict[str, Any]:
|
|
9
|
+
"""Decode JWT payload without verification.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
token: JWT token string
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Decoded payload as a dictionary
|
|
16
|
+
"""
|
|
17
|
+
parts = token.split(".")
|
|
18
|
+
if len(parts) != 3:
|
|
19
|
+
raise ValueError("Invalid JWT format")
|
|
20
|
+
|
|
21
|
+
payload = parts[1]
|
|
22
|
+
# Add padding if needed
|
|
23
|
+
padding = 4 - len(payload) % 4
|
|
24
|
+
if padding != 4:
|
|
25
|
+
payload += "=" * padding
|
|
26
|
+
|
|
27
|
+
decoded = base64.urlsafe_b64decode(payload)
|
|
28
|
+
return json.loads(decoded)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extract_account_id(token: str) -> str:
|
|
32
|
+
"""Extract ChatGPT account ID from JWT token.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
token: JWT access token from OpenAI OAuth
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
The chatgpt_account_id from the token claims
|
|
39
|
+
"""
|
|
40
|
+
payload = decode_jwt_payload(token)
|
|
41
|
+
auth_claim = payload.get("https://api.openai.com/auth", {})
|
|
42
|
+
account_id = auth_claim.get("chatgpt_account_id")
|
|
43
|
+
if not account_id:
|
|
44
|
+
raise ValueError("chatgpt_account_id not found in token")
|
|
45
|
+
return account_id
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""OAuth PKCE flow for Codex authentication."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import secrets
|
|
6
|
+
import time
|
|
7
|
+
import webbrowser
|
|
8
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
9
|
+
from threading import Thread
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import parse_qs, urlencode, urlparse
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from klaude_code.auth.codex.exceptions import CodexOAuthError
|
|
16
|
+
from klaude_code.auth.codex.jwt_utils import extract_account_id
|
|
17
|
+
from klaude_code.auth.codex.token_manager import CodexAuthState, CodexTokenManager
|
|
18
|
+
|
|
19
|
+
# OAuth configuration
|
|
20
|
+
CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
21
|
+
AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize"
|
|
22
|
+
TOKEN_URL = "https://auth.openai.com/oauth/token"
|
|
23
|
+
REDIRECT_URI = "http://localhost:1455/auth/callback"
|
|
24
|
+
REDIRECT_PORT = 1455
|
|
25
|
+
SCOPE = "openid profile email offline_access"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def generate_code_verifier() -> str:
|
|
29
|
+
"""Generate a random code verifier for PKCE."""
|
|
30
|
+
return secrets.token_urlsafe(64)[:128]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_code_challenge(verifier: str) -> str:
|
|
34
|
+
"""Generate code challenge from verifier using S256 method."""
|
|
35
|
+
digest = hashlib.sha256(verifier.encode()).digest()
|
|
36
|
+
return base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_authorize_url(code_challenge: str, state: str) -> str:
|
|
40
|
+
"""Build the authorization URL with all required parameters."""
|
|
41
|
+
params = {
|
|
42
|
+
"response_type": "code",
|
|
43
|
+
"client_id": CLIENT_ID,
|
|
44
|
+
"redirect_uri": REDIRECT_URI,
|
|
45
|
+
"scope": SCOPE,
|
|
46
|
+
"code_challenge": code_challenge,
|
|
47
|
+
"code_challenge_method": "S256",
|
|
48
|
+
"state": state,
|
|
49
|
+
"id_token_add_organizations": "true",
|
|
50
|
+
"codex_cli_simplified_flow": "true",
|
|
51
|
+
"originator": "codex_cli_rs",
|
|
52
|
+
}
|
|
53
|
+
return f"{AUTHORIZE_URL}?{urlencode(params)}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
57
|
+
"""HTTP request handler for OAuth callback."""
|
|
58
|
+
|
|
59
|
+
code: str | None = None
|
|
60
|
+
state: str | None = None
|
|
61
|
+
error: str | None = None
|
|
62
|
+
|
|
63
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
64
|
+
"""Suppress HTTP server logs."""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def do_GET(self) -> None:
|
|
68
|
+
"""Handle GET request from OAuth callback."""
|
|
69
|
+
parsed = urlparse(self.path)
|
|
70
|
+
params = parse_qs(parsed.query)
|
|
71
|
+
|
|
72
|
+
OAuthCallbackHandler.code = params.get("code", [None])[0]
|
|
73
|
+
OAuthCallbackHandler.state = params.get("state", [None])[0]
|
|
74
|
+
OAuthCallbackHandler.error = params.get("error", [None])[0]
|
|
75
|
+
|
|
76
|
+
self.send_response(200)
|
|
77
|
+
self.send_header("Content-Type", "text/html")
|
|
78
|
+
self.end_headers()
|
|
79
|
+
|
|
80
|
+
if OAuthCallbackHandler.error:
|
|
81
|
+
html = """
|
|
82
|
+
<html><body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
|
83
|
+
<h1>Authentication Failed</h1>
|
|
84
|
+
<p>Error: {}</p>
|
|
85
|
+
<p>Please close this window and try again.</p>
|
|
86
|
+
</body></html>
|
|
87
|
+
""".format(OAuthCallbackHandler.error)
|
|
88
|
+
else:
|
|
89
|
+
html = """
|
|
90
|
+
<html><body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
|
91
|
+
<h1>Authentication Successful!</h1>
|
|
92
|
+
<p>You can close this window now.</p>
|
|
93
|
+
<script>setTimeout(function() { window.close(); }, 2000);</script>
|
|
94
|
+
</body></html>
|
|
95
|
+
"""
|
|
96
|
+
self.wfile.write(html.encode())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class CodexOAuth:
|
|
100
|
+
"""Handle OAuth PKCE flow for Codex authentication."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, token_manager: CodexTokenManager | None = None):
|
|
103
|
+
self.token_manager = token_manager or CodexTokenManager()
|
|
104
|
+
|
|
105
|
+
def login(self) -> CodexAuthState:
|
|
106
|
+
"""Run the complete OAuth login flow."""
|
|
107
|
+
# Generate PKCE parameters
|
|
108
|
+
verifier = generate_code_verifier()
|
|
109
|
+
challenge = generate_code_challenge(verifier)
|
|
110
|
+
state = secrets.token_urlsafe(32)
|
|
111
|
+
|
|
112
|
+
# Build authorization URL
|
|
113
|
+
auth_url = build_authorize_url(challenge, state)
|
|
114
|
+
|
|
115
|
+
# Start callback server
|
|
116
|
+
OAuthCallbackHandler.code = None
|
|
117
|
+
OAuthCallbackHandler.state = None
|
|
118
|
+
OAuthCallbackHandler.error = None
|
|
119
|
+
|
|
120
|
+
server = HTTPServer(("localhost", REDIRECT_PORT), OAuthCallbackHandler)
|
|
121
|
+
server_thread = Thread(target=server.handle_request)
|
|
122
|
+
server_thread.start()
|
|
123
|
+
|
|
124
|
+
# Open browser for user to authenticate
|
|
125
|
+
webbrowser.open(auth_url)
|
|
126
|
+
|
|
127
|
+
# Wait for callback
|
|
128
|
+
server_thread.join(timeout=300) # 5 minute timeout
|
|
129
|
+
server.server_close()
|
|
130
|
+
|
|
131
|
+
# Check for errors
|
|
132
|
+
if OAuthCallbackHandler.error:
|
|
133
|
+
raise CodexOAuthError(f"OAuth error: {OAuthCallbackHandler.error}")
|
|
134
|
+
|
|
135
|
+
if not OAuthCallbackHandler.code:
|
|
136
|
+
raise CodexOAuthError("No authorization code received")
|
|
137
|
+
|
|
138
|
+
if OAuthCallbackHandler.state is None or OAuthCallbackHandler.state != state:
|
|
139
|
+
raise CodexOAuthError("OAuth state mismatch")
|
|
140
|
+
|
|
141
|
+
# Exchange code for tokens
|
|
142
|
+
auth_state = self._exchange_code(OAuthCallbackHandler.code, verifier)
|
|
143
|
+
|
|
144
|
+
# Save tokens
|
|
145
|
+
self.token_manager.save(auth_state)
|
|
146
|
+
|
|
147
|
+
return auth_state
|
|
148
|
+
|
|
149
|
+
def _exchange_code(self, code: str, verifier: str) -> CodexAuthState:
|
|
150
|
+
"""Exchange authorization code for tokens."""
|
|
151
|
+
data = {
|
|
152
|
+
"grant_type": "authorization_code",
|
|
153
|
+
"client_id": CLIENT_ID,
|
|
154
|
+
"code": code,
|
|
155
|
+
"redirect_uri": REDIRECT_URI,
|
|
156
|
+
"code_verifier": verifier,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
with httpx.Client() as client:
|
|
160
|
+
response = client.post(TOKEN_URL, data=data)
|
|
161
|
+
|
|
162
|
+
if response.status_code != 200:
|
|
163
|
+
raise CodexOAuthError(f"Token exchange failed: {response.text}")
|
|
164
|
+
|
|
165
|
+
tokens = response.json()
|
|
166
|
+
access_token = tokens["access_token"]
|
|
167
|
+
refresh_token = tokens["refresh_token"]
|
|
168
|
+
expires_in = tokens.get("expires_in", 3600)
|
|
169
|
+
|
|
170
|
+
account_id = extract_account_id(access_token)
|
|
171
|
+
|
|
172
|
+
return CodexAuthState(
|
|
173
|
+
access_token=access_token,
|
|
174
|
+
refresh_token=refresh_token,
|
|
175
|
+
expires_at=int(time.time()) + expires_in,
|
|
176
|
+
account_id=account_id,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def refresh(self) -> CodexAuthState:
|
|
180
|
+
"""Refresh the access token using refresh token."""
|
|
181
|
+
state = self.token_manager.get_state()
|
|
182
|
+
if state is None:
|
|
183
|
+
from klaude_code.auth.codex.exceptions import CodexNotLoggedInError
|
|
184
|
+
|
|
185
|
+
raise CodexNotLoggedInError("Not logged in to Codex. Run 'klaude login codex' first.")
|
|
186
|
+
|
|
187
|
+
data = {
|
|
188
|
+
"grant_type": "refresh_token",
|
|
189
|
+
"client_id": CLIENT_ID,
|
|
190
|
+
"refresh_token": state.refresh_token,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
with httpx.Client() as client:
|
|
194
|
+
response = client.post(TOKEN_URL, data=data)
|
|
195
|
+
|
|
196
|
+
if response.status_code != 200:
|
|
197
|
+
from klaude_code.auth.codex.exceptions import CodexTokenExpiredError
|
|
198
|
+
|
|
199
|
+
raise CodexTokenExpiredError(f"Token refresh failed: {response.text}")
|
|
200
|
+
|
|
201
|
+
tokens = response.json()
|
|
202
|
+
access_token = tokens["access_token"]
|
|
203
|
+
refresh_token = tokens.get("refresh_token", state.refresh_token)
|
|
204
|
+
expires_in = tokens.get("expires_in", 3600)
|
|
205
|
+
|
|
206
|
+
account_id = extract_account_id(access_token)
|
|
207
|
+
|
|
208
|
+
new_state = CodexAuthState(
|
|
209
|
+
access_token=access_token,
|
|
210
|
+
refresh_token=refresh_token,
|
|
211
|
+
expires_at=int(time.time()) + expires_in,
|
|
212
|
+
account_id=account_id,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
self.token_manager.save(new_state)
|
|
216
|
+
return new_state
|
|
217
|
+
|
|
218
|
+
def ensure_valid_token(self) -> str:
|
|
219
|
+
"""Ensure we have a valid access token, refreshing if needed."""
|
|
220
|
+
state = self.token_manager.get_state()
|
|
221
|
+
if state is None:
|
|
222
|
+
from klaude_code.auth.codex.exceptions import CodexNotLoggedInError
|
|
223
|
+
|
|
224
|
+
raise CodexNotLoggedInError("Not logged in to Codex. Run 'klaude login codex' first.")
|
|
225
|
+
|
|
226
|
+
if state.is_expired():
|
|
227
|
+
state = self.refresh()
|
|
228
|
+
|
|
229
|
+
return state.access_token
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Token storage and management for Codex authentication."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CodexAuthState(BaseModel):
|
|
11
|
+
"""Stored authentication state for Codex."""
|
|
12
|
+
|
|
13
|
+
access_token: str
|
|
14
|
+
refresh_token: str
|
|
15
|
+
expires_at: int # Unix timestamp
|
|
16
|
+
account_id: str
|
|
17
|
+
|
|
18
|
+
def is_expired(self, buffer_seconds: int = 300) -> bool:
|
|
19
|
+
"""Check if token is expired or will expire soon."""
|
|
20
|
+
return time.time() + buffer_seconds >= self.expires_at
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
CODEX_AUTH_FILE = Path.home() / ".klaude" / "codex-auth.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CodexTokenManager:
|
|
27
|
+
"""Manage Codex OAuth tokens."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, auth_file: Path | None = None):
|
|
30
|
+
self.auth_file = auth_file or CODEX_AUTH_FILE
|
|
31
|
+
self._state: CodexAuthState | None = None
|
|
32
|
+
|
|
33
|
+
def load(self) -> CodexAuthState | None:
|
|
34
|
+
"""Load authentication state from file."""
|
|
35
|
+
if not self.auth_file.exists():
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
data = json.loads(self.auth_file.read_text())
|
|
40
|
+
self._state = CodexAuthState.model_validate(data)
|
|
41
|
+
return self._state
|
|
42
|
+
except (json.JSONDecodeError, ValueError):
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def save(self, state: CodexAuthState) -> None:
|
|
46
|
+
"""Save authentication state to file."""
|
|
47
|
+
self.auth_file.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
self.auth_file.write_text(state.model_dump_json(indent=2))
|
|
49
|
+
self._state = state
|
|
50
|
+
|
|
51
|
+
def delete(self) -> None:
|
|
52
|
+
"""Delete stored tokens."""
|
|
53
|
+
if self.auth_file.exists():
|
|
54
|
+
self.auth_file.unlink()
|
|
55
|
+
self._state = None
|
|
56
|
+
|
|
57
|
+
def is_logged_in(self) -> bool:
|
|
58
|
+
"""Check if user is logged in."""
|
|
59
|
+
state = self._state or self.load()
|
|
60
|
+
return state is not None
|
|
61
|
+
|
|
62
|
+
def get_state(self) -> CodexAuthState | None:
|
|
63
|
+
"""Get current authentication state."""
|
|
64
|
+
if self._state is None:
|
|
65
|
+
self._state = self.load()
|
|
66
|
+
return self._state
|
|
67
|
+
|
|
68
|
+
def get_access_token(self) -> str:
|
|
69
|
+
"""Get access token, raising if not logged in."""
|
|
70
|
+
state = self.get_state()
|
|
71
|
+
if state is None:
|
|
72
|
+
from klaude_code.auth.codex.exceptions import CodexNotLoggedInError
|
|
73
|
+
|
|
74
|
+
raise CodexNotLoggedInError("Not logged in to Codex. Run 'klaude login codex' first.")
|
|
75
|
+
return state.access_token
|
|
76
|
+
|
|
77
|
+
def get_account_id(self) -> str:
|
|
78
|
+
"""Get account ID, raising if not logged in."""
|
|
79
|
+
state = self.get_state()
|
|
80
|
+
if state is None:
|
|
81
|
+
from klaude_code.auth.codex.exceptions import CodexNotLoggedInError
|
|
82
|
+
|
|
83
|
+
raise CodexNotLoggedInError("Not logged in to Codex. Run 'klaude login codex' first.")
|
|
84
|
+
return state.account_id
|
klaude_code/cli/main.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import datetime
|
|
2
3
|
import os
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
@@ -43,6 +44,68 @@ register_session_commands(session_app)
|
|
|
43
44
|
app.add_typer(session_app, name="session")
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
@app.command("login")
|
|
48
|
+
def login_command(
|
|
49
|
+
provider: str = typer.Argument("codex", help="Provider to login (codex)"),
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Login to a provider using OAuth."""
|
|
52
|
+
if provider.lower() != "codex":
|
|
53
|
+
log((f"Error: Unknown provider '{provider}'. Currently only 'codex' is supported.", "red"))
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
|
|
56
|
+
from klaude_code.auth.codex.oauth import CodexOAuth
|
|
57
|
+
from klaude_code.auth.codex.token_manager import CodexTokenManager
|
|
58
|
+
|
|
59
|
+
token_manager = CodexTokenManager()
|
|
60
|
+
|
|
61
|
+
# Check if already logged in
|
|
62
|
+
if token_manager.is_logged_in():
|
|
63
|
+
state = token_manager.get_state()
|
|
64
|
+
if state and not state.is_expired():
|
|
65
|
+
log(("You are already logged in to Codex.", "green"))
|
|
66
|
+
log(f" Account ID: {state.account_id[:8]}...")
|
|
67
|
+
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.timezone.utc)
|
|
68
|
+
log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
69
|
+
if not typer.confirm("Do you want to re-login?"):
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
log("Starting Codex OAuth login flow...")
|
|
73
|
+
log("A browser window will open for authentication.")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
oauth = CodexOAuth(token_manager)
|
|
77
|
+
state = oauth.login()
|
|
78
|
+
log(("Login successful!", "green"))
|
|
79
|
+
log(f" Account ID: {state.account_id[:8]}...")
|
|
80
|
+
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.timezone.utc)
|
|
81
|
+
log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
log((f"Login failed: {e}", "red"))
|
|
84
|
+
raise typer.Exit(1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command("logout")
|
|
88
|
+
def logout_command(
|
|
89
|
+
provider: str = typer.Argument("codex", help="Provider to logout (codex)"),
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Logout from a provider."""
|
|
92
|
+
if provider.lower() != "codex":
|
|
93
|
+
log((f"Error: Unknown provider '{provider}'. Currently only 'codex' is supported.", "red"))
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
|
|
96
|
+
from klaude_code.auth.codex.token_manager import CodexTokenManager
|
|
97
|
+
|
|
98
|
+
token_manager = CodexTokenManager()
|
|
99
|
+
|
|
100
|
+
if not token_manager.is_logged_in():
|
|
101
|
+
log("You are not logged in to Codex.")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if typer.confirm("Are you sure you want to logout from Codex?"):
|
|
105
|
+
token_manager.delete()
|
|
106
|
+
log(("Logged out from Codex.", "green"))
|
|
107
|
+
|
|
108
|
+
|
|
46
109
|
@app.command("list")
|
|
47
110
|
def list_models() -> None:
|
|
48
111
|
"""List all models and providers configuration"""
|
|
@@ -13,11 +13,18 @@ def accumulate_session_usage(session: Session) -> tuple[model.Usage, int]:
|
|
|
13
13
|
"""
|
|
14
14
|
total = model.Usage()
|
|
15
15
|
task_count = 0
|
|
16
|
+
first_currency_set = False
|
|
16
17
|
|
|
17
18
|
for item in session.conversation_history:
|
|
18
19
|
if isinstance(item, model.ResponseMetadataItem) and item.usage:
|
|
19
20
|
task_count += 1
|
|
20
21
|
usage = item.usage
|
|
22
|
+
|
|
23
|
+
# Set currency from first usage item
|
|
24
|
+
if not first_currency_set and usage.currency:
|
|
25
|
+
total.currency = usage.currency
|
|
26
|
+
first_currency_set = True
|
|
27
|
+
|
|
21
28
|
total.input_tokens += usage.input_tokens
|
|
22
29
|
total.cached_tokens += usage.cached_tokens
|
|
23
30
|
total.reasoning_tokens += usage.reasoning_tokens
|
|
@@ -50,13 +57,14 @@ def _format_tokens(tokens: int) -> str:
|
|
|
50
57
|
return str(tokens)
|
|
51
58
|
|
|
52
59
|
|
|
53
|
-
def _format_cost(cost: float | None) -> str:
|
|
54
|
-
"""Format cost
|
|
60
|
+
def _format_cost(cost: float | None, currency: str = "USD") -> str:
|
|
61
|
+
"""Format cost with currency symbol."""
|
|
55
62
|
if cost is None:
|
|
56
63
|
return "-"
|
|
64
|
+
symbol = "¥" if currency == "CNY" else "$"
|
|
57
65
|
if cost < 0.01:
|
|
58
|
-
return f"
|
|
59
|
-
return f"
|
|
66
|
+
return f"{symbol}{cost:.4f}"
|
|
67
|
+
return f"{symbol}{cost:.2f}"
|
|
60
68
|
|
|
61
69
|
|
|
62
70
|
def format_status_content(usage: model.Usage) -> str:
|
|
@@ -70,7 +78,7 @@ def format_status_content(usage: model.Usage) -> str:
|
|
|
70
78
|
parts.append(f"Total: {_format_tokens(usage.total_tokens)}")
|
|
71
79
|
|
|
72
80
|
if usage.total_cost is not None:
|
|
73
|
-
parts.append(f"Cost: {_format_cost(usage.total_cost)}")
|
|
81
|
+
parts.append(f"Cost: {_format_cost(usage.total_cost, usage.currency)}")
|
|
74
82
|
|
|
75
83
|
return ", ".join(parts)
|
|
76
84
|
|
klaude_code/config/list_model.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
1
3
|
from rich.console import Console, Group
|
|
2
4
|
from rich.panel import Panel
|
|
3
5
|
from rich.table import Table
|
|
@@ -8,6 +10,51 @@ from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
|
|
|
8
10
|
from klaude_code.ui.rich.theme import ThemeKey, get_theme
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
def _display_codex_status(console: Console) -> None:
|
|
14
|
+
"""Display Codex OAuth login status."""
|
|
15
|
+
from klaude_code.auth.codex.token_manager import CodexTokenManager
|
|
16
|
+
|
|
17
|
+
token_manager = CodexTokenManager()
|
|
18
|
+
state = token_manager.get_state()
|
|
19
|
+
|
|
20
|
+
if state is None:
|
|
21
|
+
console.print(
|
|
22
|
+
Text.assemble(
|
|
23
|
+
("Codex Status: ", "bold"),
|
|
24
|
+
("Not logged in", ThemeKey.CONFIG_STATUS_ERROR),
|
|
25
|
+
(" (run 'klaude login codex' to authenticate)", "dim"),
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
elif state.is_expired():
|
|
29
|
+
console.print(
|
|
30
|
+
Text.assemble(
|
|
31
|
+
("Codex Status: ", "bold"),
|
|
32
|
+
("Token expired", ThemeKey.CONFIG_STATUS_ERROR),
|
|
33
|
+
(" (run 'klaude login codex' to re-authenticate)", "dim"),
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.timezone.utc)
|
|
38
|
+
console.print(
|
|
39
|
+
Text.assemble(
|
|
40
|
+
("Codex Status: ", "bold"),
|
|
41
|
+
("Logged in", ThemeKey.CONFIG_STATUS_OK),
|
|
42
|
+
(f" (account: {state.account_id[:8]}..., expires: {expires_dt.strftime('%Y-%m-%d %H:%M UTC')})", "dim"),
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
console.print(
|
|
47
|
+
Text.assemble(
|
|
48
|
+
("Visit ", "dim"),
|
|
49
|
+
(
|
|
50
|
+
"https://chatgpt.com/codex/settings/usage",
|
|
51
|
+
"blue underline link https://chatgpt.com/codex/settings/usage",
|
|
52
|
+
),
|
|
53
|
+
(" for up-to-date information on rate limits and credits", "dim"),
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
11
58
|
def mask_api_key(api_key: str | None) -> str:
|
|
12
59
|
"""Mask API key to show only first 6 and last 6 characters with *** in between"""
|
|
13
60
|
if not api_key or api_key == "N/A":
|
|
@@ -160,3 +207,9 @@ def display_models_and_providers(config: Config):
|
|
|
160
207
|
(sub_model_name, ThemeKey.CONFIG_STATUS_PRIMARY),
|
|
161
208
|
)
|
|
162
209
|
)
|
|
210
|
+
|
|
211
|
+
# Display Codex login status if any codex provider is configured
|
|
212
|
+
has_codex_provider = any(p.protocol.value == "codex" for p in config.provider_list)
|
|
213
|
+
if has_codex_provider:
|
|
214
|
+
console.print()
|
|
215
|
+
_display_codex_status(console)
|
klaude_code/core/prompt.py
CHANGED
|
@@ -13,7 +13,8 @@ COMMAND_DESCRIPTIONS: dict[str, str] = {
|
|
|
13
13
|
|
|
14
14
|
# Mapping from logical prompt keys to resource file paths under the core/prompt directory.
|
|
15
15
|
PROMPT_FILES: dict[str, str] = {
|
|
16
|
-
"
|
|
16
|
+
"main_gpt_5_1": "prompts/prompt-codex-gpt-5-1.md",
|
|
17
|
+
"main_gpt_5_1_codex_max": "prompts/prompt-codex-gpt-5-1-codex-max.md",
|
|
17
18
|
"main_claude": "prompts/prompt-claude-code.md",
|
|
18
19
|
"main_gemini": "prompts/prompt-gemini.md", # https://ai.google.dev/gemini-api/docs/prompting-strategies?hl=zh-cn#agentic-si-template
|
|
19
20
|
# Sub-agent prompts keyed by their name
|
|
@@ -39,8 +40,10 @@ def get_system_prompt(model_name: str, sub_agent_type: str | None = None) -> str
|
|
|
39
40
|
|
|
40
41
|
if sub_agent_type is None:
|
|
41
42
|
match model_name:
|
|
43
|
+
case "gpt-5.1-codex-max":
|
|
44
|
+
file_key = "main_gpt_5_1_codex_max"
|
|
42
45
|
case name if "gpt-5" in name:
|
|
43
|
-
file_key = "
|
|
46
|
+
file_key = "main_gpt_5_1"
|
|
44
47
|
case name if "gemini" in name:
|
|
45
48
|
file_key = "main_gemini"
|
|
46
49
|
case _:
|
|
@@ -53,18 +56,11 @@ def get_system_prompt(model_name: str, sub_agent_type: str | None = None) -> str
|
|
|
53
56
|
except KeyError as exc:
|
|
54
57
|
raise ValueError(f"Unknown prompt key: {file_key}") from exc
|
|
55
58
|
|
|
56
|
-
base_prompt = (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
working_directory=str(cwd),
|
|
62
|
-
date=today,
|
|
63
|
-
is_git_repo=is_git_repo,
|
|
64
|
-
model_name=model_name,
|
|
65
|
-
)
|
|
66
|
-
.strip()
|
|
67
|
-
)
|
|
59
|
+
base_prompt = files(__package__).joinpath(prompt_path).read_text(encoding="utf-8").strip()
|
|
60
|
+
|
|
61
|
+
if model_name == "gpt-5.1-codex-max":
|
|
62
|
+
# Do not add env info for gpt-5.1-codex-max
|
|
63
|
+
return base_prompt
|
|
68
64
|
|
|
69
65
|
env_lines: list[str] = [
|
|
70
66
|
"",
|