klaude-code 1.2.6__py3-none-any.whl → 1.2.7__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.
Files changed (37) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/main.py +63 -0
  8. klaude_code/command/status_cmd.py +13 -5
  9. klaude_code/config/list_model.py +53 -0
  10. klaude_code/core/prompt.py +10 -14
  11. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  12. klaude_code/core/prompts/prompt-subagent-explore.md +3 -1
  13. klaude_code/core/reminders.py +14 -5
  14. klaude_code/core/task.py +1 -0
  15. klaude_code/core/tool/truncation.py +4 -0
  16. klaude_code/llm/__init__.py +2 -0
  17. klaude_code/llm/anthropic/input.py +25 -10
  18. klaude_code/llm/codex/__init__.py +5 -0
  19. klaude_code/llm/codex/client.py +116 -0
  20. klaude_code/llm/responses/client.py +153 -138
  21. klaude_code/llm/usage.py +3 -0
  22. klaude_code/protocol/llm_param.py +3 -1
  23. klaude_code/protocol/model.py +2 -1
  24. klaude_code/protocol/sub_agent.py +2 -1
  25. klaude_code/session/export.py +9 -14
  26. klaude_code/session/templates/export_session.html +5 -0
  27. klaude_code/ui/modes/repl/completers.py +41 -8
  28. klaude_code/ui/modes/repl/event_handler.py +15 -23
  29. klaude_code/ui/renderers/developer.py +9 -8
  30. klaude_code/ui/renderers/metadata.py +9 -5
  31. klaude_code/ui/renderers/user_input.py +23 -10
  32. klaude_code/ui/rich/theme.py +2 -0
  33. {klaude_code-1.2.6.dist-info → klaude_code-1.2.7.dist-info}/METADATA +1 -1
  34. {klaude_code-1.2.6.dist-info → klaude_code-1.2.7.dist-info}/RECORD +37 -28
  35. /klaude_code/core/prompts/{prompt-codex.md → prompt-codex-gpt-5-1.md} +0 -0
  36. {klaude_code-1.2.6.dist-info → klaude_code-1.2.7.dist-info}/WHEEL +0 -0
  37. {klaude_code-1.2.6.dist-info → klaude_code-1.2.7.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 in USD."""
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"${cost:.4f}"
59
- return f"${cost:.2f}"
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
 
@@ -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)
@@ -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
- "main_codex": "prompts/prompt-codex.md",
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 = "main_codex"
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
- files(__package__)
58
- .joinpath(prompt_path)
59
- .read_text(encoding="utf-8")
60
- .format(
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
  "",