klaude-code 1.2.6__py3-none-any.whl → 1.8.0__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 (205) 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/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +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 = f"""
82
+ <html><body style="font-family: sans-serif; text-align: center; padding: 50px;">
83
+ <h1>Authentication Failed</h1>
84
+ <p>Error: {OAuthCallbackHandler.error}</p>
85
+ <p>Please close this window and try again.</p>
86
+ </body></html>
87
+ """
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
@@ -0,0 +1,73 @@
1
+ """Authentication commands for CLI."""
2
+
3
+ import datetime
4
+
5
+ import typer
6
+
7
+ from klaude_code.trace import log
8
+
9
+
10
+ def login_command(
11
+ provider: str = typer.Argument("codex", help="Provider to login (codex)"),
12
+ ) -> None:
13
+ """Login to a provider using OAuth."""
14
+ if provider.lower() != "codex":
15
+ log((f"Error: Unknown provider '{provider}'. Currently only 'codex' is supported.", "red"))
16
+ raise typer.Exit(1)
17
+
18
+ from klaude_code.auth.codex.oauth import CodexOAuth
19
+ from klaude_code.auth.codex.token_manager import CodexTokenManager
20
+
21
+ token_manager = CodexTokenManager()
22
+
23
+ # Check if already logged in
24
+ if token_manager.is_logged_in():
25
+ state = token_manager.get_state()
26
+ if state and not state.is_expired():
27
+ log(("You are already logged in to Codex.", "green"))
28
+ log(f" Account ID: {state.account_id[:8]}...")
29
+ expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
30
+ log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
31
+ if not typer.confirm("Do you want to re-login?"):
32
+ return
33
+
34
+ log("Starting Codex OAuth login flow...")
35
+ log("A browser window will open for authentication.")
36
+
37
+ try:
38
+ oauth = CodexOAuth(token_manager)
39
+ state = oauth.login()
40
+ log(("Login successful!", "green"))
41
+ log(f" Account ID: {state.account_id[:8]}...")
42
+ expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
43
+ log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
44
+ except Exception as e:
45
+ log((f"Login failed: {e}", "red"))
46
+ raise typer.Exit(1) from None
47
+
48
+
49
+ def logout_command(
50
+ provider: str = typer.Argument("codex", help="Provider to logout (codex)"),
51
+ ) -> None:
52
+ """Logout from a provider."""
53
+ if provider.lower() != "codex":
54
+ log((f"Error: Unknown provider '{provider}'. Currently only 'codex' is supported.", "red"))
55
+ raise typer.Exit(1)
56
+
57
+ from klaude_code.auth.codex.token_manager import CodexTokenManager
58
+
59
+ token_manager = CodexTokenManager()
60
+
61
+ if not token_manager.is_logged_in():
62
+ log("You are not logged in to Codex.")
63
+ return
64
+
65
+ if typer.confirm("Are you sure you want to logout from Codex?"):
66
+ token_manager.delete()
67
+ log(("Logged out from Codex.", "green"))
68
+
69
+
70
+ def register_auth_commands(app: typer.Typer) -> None:
71
+ """Register auth commands to the given Typer app."""
72
+ app.command("login")(login_command)
73
+ app.command("logout")(logout_command)
@@ -0,0 +1,91 @@
1
+ """Configuration commands for CLI."""
2
+
3
+ import os
4
+ import subprocess
5
+ import sys
6
+
7
+ import typer
8
+
9
+ from klaude_code.config import config_path, create_example_config, example_config_path, load_config
10
+ from klaude_code.trace import log
11
+
12
+
13
+ def list_models() -> None:
14
+ """List all models and providers configuration"""
15
+ from klaude_code.cli.list_model import display_models_and_providers
16
+ from klaude_code.ui.terminal.color import is_light_terminal_background
17
+
18
+ config = load_config()
19
+
20
+ # Auto-detect theme when not explicitly set in config, to match other CLI entrypoints.
21
+ if config.theme is None:
22
+ detected = is_light_terminal_background()
23
+ if detected is True:
24
+ config.theme = "light"
25
+ elif detected is False:
26
+ config.theme = "dark"
27
+
28
+ display_models_and_providers(config)
29
+
30
+
31
+ def edit_config() -> None:
32
+ """Open the configuration file in $EDITOR or default system editor"""
33
+ editor = os.environ.get("EDITOR")
34
+
35
+ # If no EDITOR is set, prioritize TextEdit on macOS
36
+ if not editor:
37
+ # Try common editors in order of preference on other platforms
38
+ for cmd in [
39
+ "code",
40
+ "nvim",
41
+ "vim",
42
+ "nano",
43
+ ]:
44
+ try:
45
+ subprocess.run(["which", cmd], check=True, capture_output=True)
46
+ editor = cmd
47
+ break
48
+ except (subprocess.CalledProcessError, FileNotFoundError):
49
+ continue
50
+
51
+ # If no editor found, try platform-specific defaults
52
+ if not editor:
53
+ if sys.platform == "darwin": # macOS
54
+ editor = "open"
55
+ elif sys.platform == "win32": # Windows
56
+ editor = "notepad"
57
+ else: # Linux and other Unix systems
58
+ editor = "xdg-open"
59
+
60
+ # Ensure config directory exists and create example config if needed
61
+ config_path.parent.mkdir(parents=True, exist_ok=True)
62
+ if create_example_config():
63
+ log(f"Created example config: {example_config_path}", style="dim")
64
+
65
+ # Decide which file to open
66
+ target_path = config_path if config_path.exists() else example_config_path
67
+ if target_path == example_config_path:
68
+ log(f"Opening example config (copy to {config_path.name} to use)", style="yellow")
69
+
70
+ try:
71
+ if editor == "open -a TextEdit":
72
+ subprocess.run(["open", "-a", "TextEdit", str(target_path)], check=True)
73
+ elif editor in ["open", "xdg-open"]:
74
+ # For open/xdg-open, we need to pass the file directly
75
+ subprocess.run([editor, str(target_path)], check=True)
76
+ else:
77
+ subprocess.run([editor, str(target_path)], check=True)
78
+ except subprocess.CalledProcessError as e:
79
+ log((f"Error: Failed to open editor: {e}", "red"))
80
+ raise typer.Exit(1) from None
81
+ except FileNotFoundError:
82
+ log((f"Error: Editor '{editor}' not found", "red"))
83
+ log("Please install a text editor or set your $EDITOR environment variable")
84
+ raise typer.Exit(1) from None
85
+
86
+
87
+ def register_config_commands(app: typer.Typer) -> None:
88
+ """Register config commands to the given Typer app."""
89
+ app.command("list")(list_models)
90
+ app.command("config")(edit_config)
91
+ app.command("conf", hidden=True)(edit_config)