klaude-code 1.8.0__py3-none-any.whl → 1.9.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 (33) hide show
  1. klaude_code/auth/base.py +101 -0
  2. klaude_code/auth/claude/__init__.py +6 -0
  3. klaude_code/auth/claude/exceptions.py +9 -0
  4. klaude_code/auth/claude/oauth.py +172 -0
  5. klaude_code/auth/claude/token_manager.py +26 -0
  6. klaude_code/auth/codex/token_manager.py +10 -50
  7. klaude_code/cli/auth_cmd.py +127 -46
  8. klaude_code/cli/config_cmd.py +4 -2
  9. klaude_code/cli/cost_cmd.py +14 -9
  10. klaude_code/cli/list_model.py +248 -200
  11. klaude_code/command/prompt-commit.md +73 -0
  12. klaude_code/config/assets/builtin_config.yaml +36 -3
  13. klaude_code/config/config.py +24 -5
  14. klaude_code/config/thinking.py +4 -4
  15. klaude_code/core/prompt.py +1 -1
  16. klaude_code/llm/anthropic/client.py +28 -3
  17. klaude_code/llm/claude/__init__.py +3 -0
  18. klaude_code/llm/claude/client.py +95 -0
  19. klaude_code/llm/codex/client.py +1 -1
  20. klaude_code/llm/registry.py +3 -1
  21. klaude_code/protocol/llm_param.py +2 -1
  22. klaude_code/protocol/sub_agent/__init__.py +1 -2
  23. klaude_code/session/session.py +4 -4
  24. klaude_code/ui/renderers/metadata.py +6 -26
  25. klaude_code/ui/rich/theme.py +6 -5
  26. klaude_code/ui/utils/common.py +46 -0
  27. {klaude_code-1.8.0.dist-info → klaude_code-1.9.0.dist-info}/METADATA +25 -5
  28. {klaude_code-1.8.0.dist-info → klaude_code-1.9.0.dist-info}/RECORD +30 -25
  29. klaude_code/command/prompt-jj-describe.md +0 -32
  30. klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -22
  31. klaude_code/protocol/sub_agent/oracle.py +0 -91
  32. {klaude_code-1.8.0.dist-info → klaude_code-1.9.0.dist-info}/WHEEL +0 -0
  33. {klaude_code-1.8.0.dist-info → klaude_code-1.9.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,101 @@
1
+ """Base classes for authentication token management."""
2
+
3
+ import json
4
+ import time
5
+ from abc import ABC, abstractmethod
6
+ from pathlib import Path
7
+ from typing import Any, Generic, TypeVar, cast
8
+
9
+ from pydantic import BaseModel
10
+
11
+
12
+ KLAUDE_AUTH_FILE = Path.home() / ".klaude" / "klaude-auth.json"
13
+
14
+
15
+ class BaseAuthState(BaseModel):
16
+ """Base authentication state with common OAuth fields."""
17
+
18
+ access_token: str
19
+ refresh_token: str
20
+ expires_at: int # Unix timestamp
21
+
22
+ def is_expired(self, buffer_seconds: int = 300) -> bool:
23
+ """Check if token is expired or will expire soon."""
24
+ return time.time() + buffer_seconds >= self.expires_at
25
+
26
+
27
+ T = TypeVar("T", bound=BaseAuthState)
28
+
29
+
30
+ class BaseTokenManager(ABC, Generic[T]):
31
+ """Base class for OAuth token management."""
32
+
33
+ def __init__(self, auth_file: Path | None = None):
34
+ self.auth_file = auth_file or KLAUDE_AUTH_FILE
35
+ self._state: T | None = None
36
+
37
+ @property
38
+ @abstractmethod
39
+ def storage_key(self) -> str:
40
+ """Key used to store this auth state in the JSON file."""
41
+ ...
42
+
43
+ @abstractmethod
44
+ def _create_state(self, data: dict[str, Any]) -> T:
45
+ """Create state instance from dict data."""
46
+ ...
47
+
48
+ def _load_store(self) -> dict[str, Any]:
49
+ if not self.auth_file.exists():
50
+ return {}
51
+ try:
52
+ data: Any = json.loads(self.auth_file.read_text())
53
+ if isinstance(data, dict):
54
+ return cast(dict[str, Any], data)
55
+ return {}
56
+ except (json.JSONDecodeError, ValueError):
57
+ return {}
58
+
59
+ def _save_store(self, data: dict[str, Any]) -> None:
60
+ self.auth_file.parent.mkdir(parents=True, exist_ok=True)
61
+ self.auth_file.write_text(json.dumps(data, indent=2))
62
+
63
+ def load(self) -> T | None:
64
+ """Load authentication state from file."""
65
+ data: Any = self._load_store().get(self.storage_key)
66
+ if not isinstance(data, dict):
67
+ return None
68
+ try:
69
+ self._state = self._create_state(cast(dict[str, Any], data))
70
+ return self._state
71
+ except ValueError:
72
+ return None
73
+
74
+ def save(self, state: T) -> None:
75
+ """Save authentication state to file."""
76
+ store = self._load_store()
77
+ store[self.storage_key] = state.model_dump(mode="json")
78
+ self._save_store(store)
79
+ self._state = state
80
+
81
+ def delete(self) -> None:
82
+ """Delete stored tokens."""
83
+ store = self._load_store()
84
+ store.pop(self.storage_key, None)
85
+ if len(store) == 0:
86
+ if self.auth_file.exists():
87
+ self.auth_file.unlink()
88
+ else:
89
+ self._save_store(store)
90
+ self._state = None
91
+
92
+ def is_logged_in(self) -> bool:
93
+ """Check if user is logged in."""
94
+ state = self._state or self.load()
95
+ return state is not None
96
+
97
+ def get_state(self) -> T | None:
98
+ """Get current authentication state."""
99
+ if self._state is None:
100
+ self._state = self.load()
101
+ return self._state
@@ -0,0 +1,6 @@
1
+ """Claude (Anthropic OAuth) authentication helpers."""
2
+
3
+ from .oauth import ClaudeOAuth
4
+ from .token_manager import ClaudeAuthState, ClaudeTokenManager
5
+
6
+ __all__ = ["ClaudeAuthState", "ClaudeOAuth", "ClaudeTokenManager"]
@@ -0,0 +1,9 @@
1
+ """Exceptions for Claude OAuth authentication."""
2
+
3
+
4
+ class ClaudeAuthError(Exception):
5
+ """Base class for Claude auth errors."""
6
+
7
+
8
+ class ClaudeNotLoggedInError(ClaudeAuthError):
9
+ """Raised when no valid Claude OAuth session is available."""
@@ -0,0 +1,172 @@
1
+ """OAuth PKCE flow for Claude (Anthropic OAuth) authentication."""
2
+
3
+ import base64
4
+ import hashlib
5
+ import secrets
6
+ import time
7
+ from collections.abc import Callable
8
+
9
+ import httpx
10
+
11
+ from klaude_code.auth.claude.exceptions import ClaudeAuthError, ClaudeNotLoggedInError
12
+ from klaude_code.auth.claude.token_manager import ClaudeAuthState, ClaudeTokenManager
13
+
14
+
15
+ def _decode_base64(value: str) -> str:
16
+ return base64.b64decode(value).decode()
17
+
18
+
19
+ # OAuth configuration (Claude Pro/Max)
20
+ CLIENT_ID = _decode_base64("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl")
21
+ AUTHORIZE_URL = "https://claude.ai/oauth/authorize"
22
+ TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"
23
+ REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback"
24
+ SCOPE = "org:create_api_key user:profile user:inference"
25
+
26
+
27
+ def generate_code_verifier() -> str:
28
+ """Generate a random code verifier for PKCE."""
29
+ return secrets.token_urlsafe(64)[:128]
30
+
31
+
32
+ def generate_code_challenge(verifier: str) -> str:
33
+ """Generate code challenge from verifier using S256 method."""
34
+ digest = hashlib.sha256(verifier.encode()).digest()
35
+ return base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
36
+
37
+
38
+ def build_authorize_url(code_challenge: str, state: str) -> str:
39
+ """Build the authorization URL with all required parameters."""
40
+ # Note: the `code=true` parameter is required for the console callback flow.
41
+ params = {
42
+ "code": "true",
43
+ "client_id": CLIENT_ID,
44
+ "response_type": "code",
45
+ "redirect_uri": REDIRECT_URI,
46
+ "scope": SCOPE,
47
+ "code_challenge": code_challenge,
48
+ "code_challenge_method": "S256",
49
+ "state": state,
50
+ }
51
+
52
+ encoded = httpx.QueryParams(params)
53
+ return f"{AUTHORIZE_URL}?{encoded}"
54
+
55
+
56
+ def _parse_user_code(value: str) -> tuple[str, str | None]:
57
+ raw = value.strip()
58
+ if "#" in raw:
59
+ code, state = raw.split("#", 1)
60
+ return code.strip(), state.strip()
61
+ return raw, None
62
+
63
+
64
+ class ClaudeOAuth:
65
+ """Handle OAuth PKCE flow for Claude (Anthropic OAuth) authentication."""
66
+
67
+ def __init__(self, token_manager: ClaudeTokenManager | None = None):
68
+ self.token_manager = token_manager or ClaudeTokenManager()
69
+
70
+ def login(
71
+ self,
72
+ *,
73
+ on_auth_url: Callable[[str], None],
74
+ on_prompt_code: Callable[[], str],
75
+ ) -> ClaudeAuthState:
76
+ """Run the complete OAuth login flow."""
77
+ verifier = generate_code_verifier()
78
+ challenge = generate_code_challenge(verifier)
79
+
80
+ # Some flows require `state` to be echoed back for token exchange.
81
+ state = verifier
82
+
83
+ auth_url = build_authorize_url(challenge, state)
84
+ on_auth_url(auth_url)
85
+
86
+ raw_user_code = on_prompt_code()
87
+ code, returned_state = _parse_user_code(raw_user_code)
88
+ if not code:
89
+ raise ClaudeAuthError("No authorization code provided")
90
+
91
+ exchange_state = returned_state or state
92
+ auth_state = self._exchange_code(code=code, state=exchange_state, verifier=verifier)
93
+ self.token_manager.save(auth_state)
94
+ return auth_state
95
+
96
+ def _exchange_code(self, *, code: str, state: str, verifier: str) -> ClaudeAuthState:
97
+ """Exchange authorization code for tokens."""
98
+ payload = {
99
+ "grant_type": "authorization_code",
100
+ "client_id": CLIENT_ID,
101
+ "code": code,
102
+ "state": state,
103
+ "redirect_uri": REDIRECT_URI,
104
+ "code_verifier": verifier,
105
+ }
106
+
107
+ with httpx.Client() as client:
108
+ response = client.post(
109
+ TOKEN_URL,
110
+ json=payload,
111
+ headers={"Content-Type": "application/json"},
112
+ )
113
+
114
+ if response.status_code != 200:
115
+ raise ClaudeAuthError(f"Token exchange failed: {response.text}")
116
+
117
+ tokens = response.json()
118
+ access_token = tokens["access_token"]
119
+ refresh_token = tokens["refresh_token"]
120
+ expires_in = tokens.get("expires_in", 3600)
121
+
122
+ return ClaudeAuthState(
123
+ access_token=access_token,
124
+ refresh_token=refresh_token,
125
+ expires_at=int(time.time()) + int(expires_in),
126
+ )
127
+
128
+ def refresh(self) -> ClaudeAuthState:
129
+ """Refresh the access token using refresh token."""
130
+ state = self.token_manager.get_state()
131
+ if state is None:
132
+ raise ClaudeNotLoggedInError("Not logged in to Claude. Run 'klaude login claude' first.")
133
+
134
+ payload = {
135
+ "grant_type": "refresh_token",
136
+ "client_id": CLIENT_ID,
137
+ "refresh_token": state.refresh_token,
138
+ }
139
+
140
+ with httpx.Client() as client:
141
+ response = client.post(
142
+ TOKEN_URL,
143
+ json=payload,
144
+ headers={"Content-Type": "application/json"},
145
+ )
146
+
147
+ if response.status_code != 200:
148
+ raise ClaudeAuthError(f"Token refresh failed: {response.text}")
149
+
150
+ tokens = response.json()
151
+ access_token = tokens["access_token"]
152
+ refresh_token = tokens.get("refresh_token", state.refresh_token)
153
+ expires_in = tokens.get("expires_in", 3600)
154
+
155
+ new_state = ClaudeAuthState(
156
+ access_token=access_token,
157
+ refresh_token=refresh_token,
158
+ expires_at=int(time.time()) + int(expires_in),
159
+ )
160
+ self.token_manager.save(new_state)
161
+ return new_state
162
+
163
+ def ensure_valid_token(self) -> str:
164
+ """Ensure we have a valid access token, refreshing if needed."""
165
+ state = self.token_manager.get_state()
166
+ if state is None:
167
+ raise ClaudeNotLoggedInError("Not logged in to Claude. Run 'klaude login claude' first.")
168
+
169
+ if state.is_expired():
170
+ state = self.refresh()
171
+
172
+ return state.access_token
@@ -0,0 +1,26 @@
1
+ """Token storage and management for Claude (Anthropic OAuth) authentication."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from klaude_code.auth.base import BaseAuthState, BaseTokenManager
7
+
8
+
9
+ class ClaudeAuthState(BaseAuthState):
10
+ """Stored authentication state for Claude OAuth."""
11
+
12
+ pass
13
+
14
+
15
+ class ClaudeTokenManager(BaseTokenManager[ClaudeAuthState]):
16
+ """Manage Claude OAuth tokens."""
17
+
18
+ def __init__(self, auth_file: Path | None = None):
19
+ super().__init__(auth_file)
20
+
21
+ @property
22
+ def storage_key(self) -> str:
23
+ return "claude"
24
+
25
+ def _create_state(self, data: dict[str, Any]) -> ClaudeAuthState:
26
+ return ClaudeAuthState.model_validate(data)
@@ -1,69 +1,29 @@
1
1
  """Token storage and management for Codex authentication."""
2
2
 
3
- import json
4
- import time
5
3
  from pathlib import Path
4
+ from typing import Any
6
5
 
7
- from pydantic import BaseModel
6
+ from klaude_code.auth.base import BaseAuthState, BaseTokenManager
8
7
 
9
8
 
10
- class CodexAuthState(BaseModel):
9
+ class CodexAuthState(BaseAuthState):
11
10
  """Stored authentication state for Codex."""
12
11
 
13
- access_token: str
14
- refresh_token: str
15
- expires_at: int # Unix timestamp
16
12
  account_id: str
17
13
 
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
14
 
22
-
23
- CODEX_AUTH_FILE = Path.home() / ".klaude" / "codex-auth.json"
24
-
25
-
26
- class CodexTokenManager:
15
+ class CodexTokenManager(BaseTokenManager[CodexAuthState]):
27
16
  """Manage Codex OAuth tokens."""
28
17
 
29
18
  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
19
+ super().__init__(auth_file)
56
20
 
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
21
+ @property
22
+ def storage_key(self) -> str:
23
+ return "codex"
61
24
 
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
25
+ def _create_state(self, data: dict[str, Any]) -> CodexAuthState:
26
+ return CodexAuthState.model_validate(data)
67
27
 
68
28
  def get_access_token(self) -> str:
69
29
  """Get access token, raising if not logged in."""
@@ -1,70 +1,151 @@
1
1
  """Authentication commands for CLI."""
2
2
 
3
3
  import datetime
4
+ import webbrowser
4
5
 
5
6
  import typer
7
+ from prompt_toolkit.styles import Style
6
8
 
7
9
  from klaude_code.trace import log
10
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
11
+
12
+ _SELECT_STYLE = Style(
13
+ [
14
+ ("instruction", "ansibrightblack"),
15
+ ("pointer", "ansigreen"),
16
+ ("highlighted", "ansigreen"),
17
+ ("text", "ansibrightblack"),
18
+ ("question", "bold"),
19
+ ]
20
+ )
21
+
22
+
23
+ def _select_provider() -> str | None:
24
+ """Display provider selection menu and return selected provider."""
25
+ items: list[SelectItem[str]] = [
26
+ SelectItem(title=[("class:text", "Claude Max/Pro Subscription\n")], value="claude", search_text="claude"),
27
+ SelectItem(title=[("class:text", "ChatGPT Codex Subscription\n")], value="codex", search_text="codex"),
28
+ ]
29
+ return select_one(
30
+ message="Select provider to login:",
31
+ items=items,
32
+ pointer="→",
33
+ style=_SELECT_STYLE,
34
+ use_search_filter=False,
35
+ )
8
36
 
9
37
 
10
38
  def login_command(
11
- provider: str = typer.Argument("codex", help="Provider to login (codex)"),
39
+ provider: str | None = typer.Argument(None, help="Provider to login (codex|claude)"),
12
40
  ) -> None:
13
41
  """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
42
+ if provider is None:
43
+ provider = _select_provider()
44
+ if provider is None:
45
+ return
46
+
47
+ match provider.lower():
48
+ case "codex":
49
+ from klaude_code.auth.codex.oauth import CodexOAuth
50
+ from klaude_code.auth.codex.token_manager import CodexTokenManager
51
+
52
+ token_manager = CodexTokenManager()
53
+
54
+ # Check if already logged in
55
+ if token_manager.is_logged_in():
56
+ state = token_manager.get_state()
57
+ if state and not state.is_expired():
58
+ log(("You are already logged in to Codex.", "green"))
59
+ log(f" Account ID: {state.account_id[:8]}...")
60
+ expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
61
+ log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
62
+ if not typer.confirm("Do you want to re-login?"):
63
+ return
64
+
65
+ log("Starting Codex OAuth login flow...")
66
+ log("A browser window will open for authentication.")
67
+
68
+ try:
69
+ oauth = CodexOAuth(token_manager)
70
+ state = oauth.login()
71
+ log(("Login successful!", "green"))
72
+ log(f" Account ID: {state.account_id[:8]}...")
73
+ expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
74
+ log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
75
+ except Exception as e:
76
+ log((f"Login failed: {e}", "red"))
77
+ raise typer.Exit(1) from None
78
+ case "claude":
79
+ from klaude_code.auth.claude.oauth import ClaudeOAuth
80
+ from klaude_code.auth.claude.token_manager import ClaudeTokenManager
81
+
82
+ token_manager = ClaudeTokenManager()
83
+
84
+ if token_manager.is_logged_in():
85
+ state = token_manager.get_state()
86
+ if state and not state.is_expired():
87
+ log(("You are already logged in to Claude.", "green"))
88
+ expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
89
+ log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
90
+ if not typer.confirm("Do you want to re-login?"):
91
+ return
92
+
93
+ log("Starting Claude OAuth login flow...")
94
+ log("A browser window will open for authentication.")
95
+ log("After login, paste the authorization code in the terminal.")
96
+
97
+ try:
98
+ oauth = ClaudeOAuth(token_manager)
99
+ state = oauth.login(
100
+ on_auth_url=lambda url: (webbrowser.open(url), None)[1],
101
+ on_prompt_code=lambda: typer.prompt(
102
+ "Paste the authorization code (format: code#state)",
103
+ prompt_suffix=": ",
104
+ ),
105
+ )
106
+ log(("Login successful!", "green"))
107
+ expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
108
+ log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
109
+ except Exception as e:
110
+ log((f"Login failed: {e}", "red"))
111
+ raise typer.Exit(1) from None
112
+ case _:
113
+ log((f"Error: Unknown provider '{provider}'. Supported: codex, claude", "red"))
114
+ raise typer.Exit(1)
47
115
 
48
116
 
49
117
  def logout_command(
50
- provider: str = typer.Argument("codex", help="Provider to logout (codex)"),
118
+ provider: str = typer.Argument("codex", help="Provider to logout (codex|claude)"),
51
119
  ) -> None:
52
120
  """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)
121
+ match provider.lower():
122
+ case "codex":
123
+ from klaude_code.auth.codex.token_manager import CodexTokenManager
56
124
 
57
- from klaude_code.auth.codex.token_manager import CodexTokenManager
125
+ token_manager = CodexTokenManager()
58
126
 
59
- token_manager = CodexTokenManager()
127
+ if not token_manager.is_logged_in():
128
+ log("You are not logged in to Codex.")
129
+ return
60
130
 
61
- if not token_manager.is_logged_in():
62
- log("You are not logged in to Codex.")
63
- return
131
+ if typer.confirm("Are you sure you want to logout from Codex?"):
132
+ token_manager.delete()
133
+ log(("Logged out from Codex.", "green"))
134
+ case "claude":
135
+ from klaude_code.auth.claude.token_manager import ClaudeTokenManager
136
+
137
+ token_manager = ClaudeTokenManager()
138
+
139
+ if not token_manager.is_logged_in():
140
+ log("You are not logged in to Claude.")
141
+ return
64
142
 
65
- if typer.confirm("Are you sure you want to logout from Codex?"):
66
- token_manager.delete()
67
- log(("Logged out from Codex.", "green"))
143
+ if typer.confirm("Are you sure you want to logout from Claude?"):
144
+ token_manager.delete()
145
+ log(("Logged out from Claude.", "green"))
146
+ case _:
147
+ log((f"Error: Unknown provider '{provider}'. Supported: codex, claude", "red"))
148
+ raise typer.Exit(1)
68
149
 
69
150
 
70
151
  def register_auth_commands(app: typer.Typer) -> None:
@@ -10,7 +10,9 @@ from klaude_code.config import config_path, create_example_config, example_confi
10
10
  from klaude_code.trace import log
11
11
 
12
12
 
13
- def list_models() -> None:
13
+ def list_models(
14
+ show_all: bool = typer.Option(False, "--all", "-a", help="Show all providers including unavailable ones"),
15
+ ) -> None:
14
16
  """List all models and providers configuration"""
15
17
  from klaude_code.cli.list_model import display_models_and_providers
16
18
  from klaude_code.ui.terminal.color import is_light_terminal_background
@@ -25,7 +27,7 @@ def list_models() -> None:
25
27
  elif detected is False:
26
28
  config.theme = "dark"
27
29
 
28
- display_models_and_providers(config)
30
+ display_models_and_providers(config, show_all=show_all)
29
31
 
30
32
 
31
33
  def edit_config() -> None:
@@ -5,8 +5,8 @@ from dataclasses import dataclass, field
5
5
  from datetime import datetime
6
6
  from pathlib import Path
7
7
 
8
- from rich.box import Box
9
8
  import typer
9
+ from rich.box import Box
10
10
  from rich.console import Console
11
11
  from rich.table import Table
12
12
 
@@ -163,13 +163,13 @@ def format_cost_dual(cost_usd: float, cost_cny: float) -> tuple[str, str]:
163
163
 
164
164
 
165
165
  def format_date_display(date_str: str) -> str:
166
- """Format date string YYYY-MM-DD to 'YYYY M-D' for table display."""
167
- parts = date_str.split("-")
168
- if len(parts) == 3:
169
- month = int(parts[1])
170
- day = int(parts[2])
171
- return f"{parts[0]} {month}-{day}"
172
- return date_str
166
+ """Format date string YYYY-MM-DD to 'YYYY M-D WEEKDAY' for table display."""
167
+ try:
168
+ dt = datetime.strptime(date_str, "%Y-%m-%d")
169
+ weekday = dt.strftime("%a").upper()
170
+ return f"{dt.year} {dt.month}-{dt.day} {weekday}"
171
+ except (ValueError, TypeError):
172
+ return date_str
173
173
 
174
174
 
175
175
  def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
@@ -178,6 +178,7 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
178
178
  title="Usage Statistics",
179
179
  show_header=True,
180
180
  header_style="bold",
181
+ border_style="bright_black dim",
181
182
  padding=(0, 1, 0, 2),
182
183
  box=ASCII_HORIZONAL,
183
184
  )
@@ -267,10 +268,14 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
267
268
  sorted_global_models = [s.model_name for s in sorted(global_by_model.values(), key=sort_by_cost)]
268
269
  first_total_row = True
269
270
  for model_name in sorted_global_models:
271
+ # Add empty row before first model to align with Total date range
272
+ if first_total_row:
273
+ table.add_row(total_label, "", "", "", "", "", "", "")
274
+ first_total_row = False
270
275
  stats = global_by_model[model_name]
271
276
  usd_str, cny_str = format_cost_dual(stats.cost_usd, stats.cost_cny)
272
277
  table.add_row(
273
- total_label if first_total_row else "",
278
+ "",
274
279
  f"- {model_name}",
275
280
  format_tokens(stats.input_tokens),
276
281
  format_tokens(stats.output_tokens),