klaude-code 1.2.13__py3-none-any.whl → 1.2.14__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/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +88 -0
- klaude_code/cli/debug.py +72 -0
- klaude_code/cli/main.py +31 -142
- klaude_code/cli/runtime.py +0 -31
- klaude_code/cli/session_cmd.py +3 -1
- klaude_code/command/model_cmd.py +1 -1
- klaude_code/config/__init__.py +0 -4
- klaude_code/config/config.py +31 -4
- klaude_code/const/__init__.py +8 -3
- klaude_code/core/agent.py +1 -1
- klaude_code/core/prompt.py +10 -5
- klaude_code/llm/client.py +4 -0
- klaude_code/llm/openrouter/client.py +5 -4
- klaude_code/protocol/events.py +2 -2
- klaude_code/protocol/sub_agent.py +0 -1
- klaude_code/session/export.py +58 -0
- klaude_code/session/session.py +35 -3
- klaude_code/session/templates/export_session.html +46 -0
- klaude_code/trace/__init__.py +2 -2
- klaude_code/trace/log.py +143 -4
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/completers.py +3 -3
- klaude_code/ui/modes/repl/renderer.py +50 -61
- klaude_code/ui/renderers/tools.py +4 -0
- klaude_code/ui/rich/theme.py +1 -1
- {klaude_code-1.2.13.dist-info → klaude_code-1.2.14.dist-info}/METADATA +1 -1
- {klaude_code-1.2.13.dist-info → klaude_code-1.2.14.dist-info}/RECORD +30 -27
- {klaude_code-1.2.13.dist-info → klaude_code-1.2.14.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.13.dist-info → klaude_code-1.2.14.dist-info}/entry_points.txt +0 -0
|
@@ -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,88 @@
|
|
|
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, 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.config.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
|
+
if config is None:
|
|
20
|
+
raise typer.Exit(1)
|
|
21
|
+
|
|
22
|
+
# Auto-detect theme when not explicitly set in config, to match other CLI entrypoints.
|
|
23
|
+
if config.theme is None:
|
|
24
|
+
detected = is_light_terminal_background()
|
|
25
|
+
if detected is True:
|
|
26
|
+
config.theme = "light"
|
|
27
|
+
elif detected is False:
|
|
28
|
+
config.theme = "dark"
|
|
29
|
+
|
|
30
|
+
display_models_and_providers(config)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def edit_config() -> None:
|
|
34
|
+
"""Open the configuration file in $EDITOR or default system editor"""
|
|
35
|
+
editor = os.environ.get("EDITOR")
|
|
36
|
+
|
|
37
|
+
# If no EDITOR is set, prioritize TextEdit on macOS
|
|
38
|
+
if not editor:
|
|
39
|
+
# Try common editors in order of preference on other platforms
|
|
40
|
+
for cmd in [
|
|
41
|
+
"code",
|
|
42
|
+
"nvim",
|
|
43
|
+
"vim",
|
|
44
|
+
"nano",
|
|
45
|
+
]:
|
|
46
|
+
try:
|
|
47
|
+
subprocess.run(["which", cmd], check=True, capture_output=True)
|
|
48
|
+
editor = cmd
|
|
49
|
+
break
|
|
50
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# If no editor found, try platform-specific defaults
|
|
54
|
+
if not editor:
|
|
55
|
+
if sys.platform == "darwin": # macOS
|
|
56
|
+
editor = "open"
|
|
57
|
+
elif sys.platform == "win32": # Windows
|
|
58
|
+
editor = "notepad"
|
|
59
|
+
else: # Linux and other Unix systems
|
|
60
|
+
editor = "xdg-open"
|
|
61
|
+
|
|
62
|
+
# Ensure config file exists
|
|
63
|
+
config = load_config()
|
|
64
|
+
if config is None:
|
|
65
|
+
raise typer.Exit(1)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
if editor == "open -a TextEdit":
|
|
69
|
+
subprocess.run(["open", "-a", "TextEdit", str(config_path)], check=True)
|
|
70
|
+
elif editor in ["open", "xdg-open"]:
|
|
71
|
+
# For open/xdg-open, we need to pass the file directly
|
|
72
|
+
subprocess.run([editor, str(config_path)], check=True)
|
|
73
|
+
else:
|
|
74
|
+
subprocess.run([editor, str(config_path)], check=True)
|
|
75
|
+
except subprocess.CalledProcessError as e:
|
|
76
|
+
log((f"Error: Failed to open editor: {e}", "red"))
|
|
77
|
+
raise typer.Exit(1) from None
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
log((f"Error: Editor '{editor}' not found", "red"))
|
|
80
|
+
log("Please install a text editor or set your $EDITOR environment variable")
|
|
81
|
+
raise typer.Exit(1) from None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def register_config_commands(app: typer.Typer) -> None:
|
|
85
|
+
"""Register config commands to the given Typer app."""
|
|
86
|
+
app.command("list")(list_models)
|
|
87
|
+
app.command("config")(edit_config)
|
|
88
|
+
app.command("conf", hidden=True)(edit_config)
|
klaude_code/cli/debug.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Debug utilities for CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from klaude_code.trace import DebugType, log
|
|
11
|
+
|
|
12
|
+
DEBUG_FILTER_HELP = "Comma-separated debug types: " + ", ".join(dt.value for dt in DebugType)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_debug_filters(raw: str | None) -> set[DebugType] | None:
|
|
16
|
+
"""Parse comma-separated debug filter string into a set of DebugType."""
|
|
17
|
+
if raw is None:
|
|
18
|
+
return None
|
|
19
|
+
filters: set[DebugType] = set()
|
|
20
|
+
for chunk in raw.split(","):
|
|
21
|
+
normalized = chunk.strip().lower().replace("-", "_")
|
|
22
|
+
if not normalized:
|
|
23
|
+
continue
|
|
24
|
+
try:
|
|
25
|
+
filters.add(DebugType(normalized))
|
|
26
|
+
except ValueError: # pragma: no cover - user input validation
|
|
27
|
+
valid_options = ", ".join(dt.value for dt in DebugType)
|
|
28
|
+
log(
|
|
29
|
+
(
|
|
30
|
+
f"Invalid debug filter '{normalized}'. Valid options: {valid_options}",
|
|
31
|
+
"red",
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
raise typer.Exit(2) from None
|
|
35
|
+
return filters or None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_debug_settings(flag: bool, raw_filters: str | None) -> tuple[bool, set[DebugType] | None]:
|
|
39
|
+
"""Resolve debug flag and filters into effective settings."""
|
|
40
|
+
filters = parse_debug_filters(raw_filters)
|
|
41
|
+
effective_flag = flag or (filters is not None)
|
|
42
|
+
return effective_flag, filters
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def open_log_file_in_editor(path: Path) -> None:
|
|
46
|
+
"""Open the given log file in a text editor without blocking the CLI."""
|
|
47
|
+
|
|
48
|
+
editor = os.environ.get("EDITOR")
|
|
49
|
+
|
|
50
|
+
if not editor:
|
|
51
|
+
for cmd in ["open", "xdg-open", "code", "nvim", "vim", "nano"]:
|
|
52
|
+
try:
|
|
53
|
+
subprocess.run(["which", cmd], check=True, capture_output=True)
|
|
54
|
+
editor = cmd
|
|
55
|
+
break
|
|
56
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
if not editor:
|
|
60
|
+
if sys.platform == "darwin":
|
|
61
|
+
editor = "open"
|
|
62
|
+
elif sys.platform == "win32":
|
|
63
|
+
editor = "notepad"
|
|
64
|
+
else:
|
|
65
|
+
editor = "xdg-open"
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
subprocess.Popen([editor, str(path)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
69
|
+
except FileNotFoundError:
|
|
70
|
+
log((f"Error: Editor '{editor}' not found", "red"))
|
|
71
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
72
|
+
log((f"Warning: failed to open log file in editor: {exc}", "yellow"))
|
klaude_code/cli/main.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import datetime
|
|
3
2
|
import os
|
|
4
|
-
import subprocess
|
|
5
3
|
import sys
|
|
6
4
|
from importlib.metadata import PackageNotFoundError
|
|
7
5
|
from importlib.metadata import version as pkg_version
|
|
6
|
+
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
import typer
|
|
10
9
|
|
|
11
|
-
from klaude_code.cli.
|
|
10
|
+
from klaude_code.cli.auth_cmd import register_auth_commands
|
|
11
|
+
from klaude_code.cli.config_cmd import register_config_commands
|
|
12
|
+
from klaude_code.cli.debug import DEBUG_FILTER_HELP, open_log_file_in_editor, resolve_debug_settings
|
|
12
13
|
from klaude_code.cli.session_cmd import register_session_commands
|
|
13
|
-
from klaude_code.config import
|
|
14
|
+
from klaude_code.config import load_config
|
|
14
15
|
from klaude_code.session import Session, resume_select_session
|
|
15
|
-
from klaude_code.trace import
|
|
16
|
-
from klaude_code.ui.terminal.color import is_light_terminal_background
|
|
16
|
+
from klaude_code.trace import prepare_debug_log_file
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def set_terminal_title(title: str) -> None:
|
|
@@ -42,142 +42,10 @@ app = typer.Typer(
|
|
|
42
42
|
no_args_is_help=False,
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
register_session_commands(
|
|
47
|
-
app
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@app.command("login")
|
|
51
|
-
def login_command(
|
|
52
|
-
provider: str = typer.Argument("codex", help="Provider to login (codex)"),
|
|
53
|
-
) -> None:
|
|
54
|
-
"""Login to a provider using OAuth."""
|
|
55
|
-
if provider.lower() != "codex":
|
|
56
|
-
log((f"Error: Unknown provider '{provider}'. Currently only 'codex' is supported.", "red"))
|
|
57
|
-
raise typer.Exit(1)
|
|
58
|
-
|
|
59
|
-
from klaude_code.auth.codex.oauth import CodexOAuth
|
|
60
|
-
from klaude_code.auth.codex.token_manager import CodexTokenManager
|
|
61
|
-
|
|
62
|
-
token_manager = CodexTokenManager()
|
|
63
|
-
|
|
64
|
-
# Check if already logged in
|
|
65
|
-
if token_manager.is_logged_in():
|
|
66
|
-
state = token_manager.get_state()
|
|
67
|
-
if state and not state.is_expired():
|
|
68
|
-
log(("You are already logged in to Codex.", "green"))
|
|
69
|
-
log(f" Account ID: {state.account_id[:8]}...")
|
|
70
|
-
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
|
|
71
|
-
log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
72
|
-
if not typer.confirm("Do you want to re-login?"):
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
log("Starting Codex OAuth login flow...")
|
|
76
|
-
log("A browser window will open for authentication.")
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
oauth = CodexOAuth(token_manager)
|
|
80
|
-
state = oauth.login()
|
|
81
|
-
log(("Login successful!", "green"))
|
|
82
|
-
log(f" Account ID: {state.account_id[:8]}...")
|
|
83
|
-
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
|
|
84
|
-
log(f" Expires: {expires_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
85
|
-
except Exception as e:
|
|
86
|
-
log((f"Login failed: {e}", "red"))
|
|
87
|
-
raise typer.Exit(1) from None
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@app.command("logout")
|
|
91
|
-
def logout_command(
|
|
92
|
-
provider: str = typer.Argument("codex", help="Provider to logout (codex)"),
|
|
93
|
-
) -> None:
|
|
94
|
-
"""Logout from a provider."""
|
|
95
|
-
if provider.lower() != "codex":
|
|
96
|
-
log((f"Error: Unknown provider '{provider}'. Currently only 'codex' is supported.", "red"))
|
|
97
|
-
raise typer.Exit(1)
|
|
98
|
-
|
|
99
|
-
from klaude_code.auth.codex.token_manager import CodexTokenManager
|
|
100
|
-
|
|
101
|
-
token_manager = CodexTokenManager()
|
|
102
|
-
|
|
103
|
-
if not token_manager.is_logged_in():
|
|
104
|
-
log("You are not logged in to Codex.")
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
if typer.confirm("Are you sure you want to logout from Codex?"):
|
|
108
|
-
token_manager.delete()
|
|
109
|
-
log(("Logged out from Codex.", "green"))
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@app.command("list")
|
|
113
|
-
def list_models() -> None:
|
|
114
|
-
"""List all models and providers configuration"""
|
|
115
|
-
config = load_config()
|
|
116
|
-
if config is None:
|
|
117
|
-
raise typer.Exit(1)
|
|
118
|
-
|
|
119
|
-
# Auto-detect theme when not explicitly set in config, to match other CLI entrypoints.
|
|
120
|
-
if config.theme is None:
|
|
121
|
-
detected = is_light_terminal_background()
|
|
122
|
-
if detected is True:
|
|
123
|
-
config.theme = "light"
|
|
124
|
-
elif detected is False:
|
|
125
|
-
config.theme = "dark"
|
|
126
|
-
|
|
127
|
-
display_models_and_providers(config)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
@app.command("config")
|
|
131
|
-
@app.command("conf", hidden=True)
|
|
132
|
-
def edit_config() -> None:
|
|
133
|
-
"""Open the configuration file in $EDITOR or default system editor"""
|
|
134
|
-
editor = os.environ.get("EDITOR")
|
|
135
|
-
|
|
136
|
-
# If no EDITOR is set, prioritize TextEdit on macOS
|
|
137
|
-
if not editor:
|
|
138
|
-
# Try common editors in order of preference on other platforms
|
|
139
|
-
for cmd in [
|
|
140
|
-
"code",
|
|
141
|
-
"nvim",
|
|
142
|
-
"vim",
|
|
143
|
-
"nano",
|
|
144
|
-
]:
|
|
145
|
-
try:
|
|
146
|
-
subprocess.run(["which", cmd], check=True, capture_output=True)
|
|
147
|
-
editor = cmd
|
|
148
|
-
break
|
|
149
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
150
|
-
continue
|
|
151
|
-
|
|
152
|
-
# If no editor found, try platform-specific defaults
|
|
153
|
-
if not editor:
|
|
154
|
-
if sys.platform == "darwin": # macOS
|
|
155
|
-
editor = "open"
|
|
156
|
-
elif sys.platform == "win32": # Windows
|
|
157
|
-
editor = "notepad"
|
|
158
|
-
else: # Linux and other Unix systems
|
|
159
|
-
editor = "xdg-open"
|
|
160
|
-
|
|
161
|
-
# Ensure config file exists
|
|
162
|
-
config = load_config()
|
|
163
|
-
if config is None:
|
|
164
|
-
raise typer.Exit(1)
|
|
165
|
-
|
|
166
|
-
try:
|
|
167
|
-
if editor == "open -a TextEdit":
|
|
168
|
-
subprocess.run(["open", "-a", "TextEdit", str(config_path)], check=True)
|
|
169
|
-
elif editor in ["open", "xdg-open"]:
|
|
170
|
-
# For open/xdg-open, we need to pass the file directly
|
|
171
|
-
subprocess.run([editor, str(config_path)], check=True)
|
|
172
|
-
else:
|
|
173
|
-
subprocess.run([editor, str(config_path)], check=True)
|
|
174
|
-
except subprocess.CalledProcessError as e:
|
|
175
|
-
log((f"Error: Failed to open editor: {e}", "red"))
|
|
176
|
-
raise typer.Exit(1) from None
|
|
177
|
-
except FileNotFoundError:
|
|
178
|
-
log((f"Error: Editor '{editor}' not found", "red"))
|
|
179
|
-
log("Please install a text editor or set your $EDITOR environment variable")
|
|
180
|
-
raise typer.Exit(1) from None
|
|
45
|
+
# Register subcommands from modules
|
|
46
|
+
register_session_commands(app)
|
|
47
|
+
register_auth_commands(app)
|
|
48
|
+
register_config_commands(app)
|
|
181
49
|
|
|
182
50
|
|
|
183
51
|
@app.command("exec")
|
|
@@ -222,6 +90,7 @@ def exec_command(
|
|
|
222
90
|
),
|
|
223
91
|
) -> None:
|
|
224
92
|
"""Execute non-interactively with provided input."""
|
|
93
|
+
from klaude_code.trace import log
|
|
225
94
|
|
|
226
95
|
# Set terminal title with current folder name
|
|
227
96
|
folder_name = os.path.basename(os.getcwd())
|
|
@@ -250,6 +119,9 @@ def exec_command(
|
|
|
250
119
|
log(("Error: No input content provided", "red"))
|
|
251
120
|
raise typer.Exit(1)
|
|
252
121
|
|
|
122
|
+
from klaude_code.cli.runtime import AppInitConfig, run_exec
|
|
123
|
+
from klaude_code.config.select_model import select_model_from_config
|
|
124
|
+
|
|
253
125
|
chosen_model = model
|
|
254
126
|
if select_model:
|
|
255
127
|
# Prefer the explicitly provided model as default; otherwise main model
|
|
@@ -263,6 +135,10 @@ def exec_command(
|
|
|
263
135
|
|
|
264
136
|
debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
|
|
265
137
|
|
|
138
|
+
log_path: Path | None = None
|
|
139
|
+
if debug_enabled:
|
|
140
|
+
log_path = prepare_debug_log_file()
|
|
141
|
+
|
|
266
142
|
init_config = AppInitConfig(
|
|
267
143
|
model=chosen_model,
|
|
268
144
|
debug=debug_enabled,
|
|
@@ -272,6 +148,9 @@ def exec_command(
|
|
|
272
148
|
stream_json=stream_json,
|
|
273
149
|
)
|
|
274
150
|
|
|
151
|
+
if log_path:
|
|
152
|
+
open_log_file_in_editor(log_path)
|
|
153
|
+
|
|
275
154
|
asyncio.run(
|
|
276
155
|
run_exec(
|
|
277
156
|
init_config=init_config,
|
|
@@ -328,6 +207,9 @@ def main_callback(
|
|
|
328
207
|
) -> None:
|
|
329
208
|
# Only run interactive mode when no subcommand is invoked
|
|
330
209
|
if ctx.invoked_subcommand is None:
|
|
210
|
+
from klaude_code.cli.runtime import AppInitConfig, run_interactive
|
|
211
|
+
from klaude_code.config.select_model import select_model_from_config
|
|
212
|
+
|
|
331
213
|
# Set terminal title with current folder name
|
|
332
214
|
folder_name = os.path.basename(os.getcwd())
|
|
333
215
|
set_terminal_title(f"{folder_name}: klaude")
|
|
@@ -352,6 +234,10 @@ def main_callback(
|
|
|
352
234
|
|
|
353
235
|
debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
|
|
354
236
|
|
|
237
|
+
log_path: Path | None = None
|
|
238
|
+
if debug_enabled:
|
|
239
|
+
log_path = prepare_debug_log_file()
|
|
240
|
+
|
|
355
241
|
init_config = AppInitConfig(
|
|
356
242
|
model=chosen_model,
|
|
357
243
|
debug=debug_enabled,
|
|
@@ -359,6 +245,9 @@ def main_callback(
|
|
|
359
245
|
debug_filters=debug_filters,
|
|
360
246
|
)
|
|
361
247
|
|
|
248
|
+
if log_path:
|
|
249
|
+
open_log_file_in_editor(log_path)
|
|
250
|
+
|
|
362
251
|
asyncio.run(
|
|
363
252
|
run_interactive(
|
|
364
253
|
init_config=init_config,
|
klaude_code/cli/runtime.py
CHANGED
|
@@ -30,37 +30,6 @@ class PrintCapable(Protocol):
|
|
|
30
30
|
def print(self, *objects: Any, style: Any | None = None, end: str = "\n") -> None: ...
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
DEBUG_FILTER_HELP = "Comma-separated debug types: " + ", ".join(dt.value for dt in DebugType)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _parse_debug_filters(raw: str | None) -> set[DebugType] | None:
|
|
37
|
-
if raw is None:
|
|
38
|
-
return None
|
|
39
|
-
filters: set[DebugType] = set()
|
|
40
|
-
for chunk in raw.split(","):
|
|
41
|
-
normalized = chunk.strip().lower().replace("-", "_")
|
|
42
|
-
if not normalized:
|
|
43
|
-
continue
|
|
44
|
-
try:
|
|
45
|
-
filters.add(DebugType(normalized))
|
|
46
|
-
except ValueError: # pragma: no cover - user input validation
|
|
47
|
-
valid_options = ", ".join(dt.value for dt in DebugType)
|
|
48
|
-
log(
|
|
49
|
-
(
|
|
50
|
-
f"Invalid debug filter '{normalized}'. Valid options: {valid_options}",
|
|
51
|
-
"red",
|
|
52
|
-
)
|
|
53
|
-
)
|
|
54
|
-
raise typer.Exit(2) from None
|
|
55
|
-
return filters or None
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def resolve_debug_settings(flag: bool, raw_filters: str | None) -> tuple[bool, set[DebugType] | None]:
|
|
59
|
-
filters = _parse_debug_filters(raw_filters)
|
|
60
|
-
effective_flag = flag or (filters is not None)
|
|
61
|
-
return effective_flag, filters
|
|
62
|
-
|
|
63
|
-
|
|
64
33
|
@dataclass
|
|
65
34
|
class AppInitConfig:
|
|
66
35
|
"""Configuration for initializing the application components."""
|
klaude_code/cli/session_cmd.py
CHANGED
|
@@ -72,7 +72,9 @@ def session_clean_all(
|
|
|
72
72
|
log(f"Deleted {deleted} session(s).")
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def register_session_commands(
|
|
75
|
+
def register_session_commands(app: typer.Typer) -> None:
|
|
76
76
|
"""Register session subcommands to the given Typer app."""
|
|
77
|
+
session_app = typer.Typer(help="Manage sessions for the current project")
|
|
77
78
|
session_app.command("clean")(session_clean)
|
|
78
79
|
session_app.command("clean-all")(session_clean_all)
|
|
80
|
+
app.add_typer(session_app, name="session")
|
klaude_code/command/model_cmd.py
CHANGED
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
from klaude_code.command.command_abc import CommandABC, CommandResult, InputAction
|
|
5
|
-
from klaude_code.config import select_model_from_config
|
|
5
|
+
from klaude_code.config.select_model import select_model_from_config
|
|
6
6
|
from klaude_code.protocol import commands, events, model
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
klaude_code/config/__init__.py
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
from .config import Config, config_path, load_config
|
|
2
|
-
from .list_model import display_models_and_providers
|
|
3
|
-
from .select_model import select_model_from_config
|
|
4
2
|
|
|
5
3
|
__all__ = [
|
|
6
4
|
"Config",
|
|
7
5
|
"config_path",
|
|
8
|
-
"display_models_and_providers",
|
|
9
6
|
"load_config",
|
|
10
|
-
"select_model_from_config",
|
|
11
7
|
]
|
klaude_code/config/config.py
CHANGED
|
@@ -71,7 +71,8 @@ class Config(BaseModel):
|
|
|
71
71
|
|
|
72
72
|
def _save_config() -> None:
|
|
73
73
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
-
|
|
74
|
+
yaml_content = yaml.dump(config_dict, default_flow_style=False, sort_keys=False)
|
|
75
|
+
_ = config_path.write_text(str(yaml_content or ""))
|
|
75
76
|
|
|
76
77
|
await asyncio.to_thread(_save_config)
|
|
77
78
|
|
|
@@ -142,8 +143,7 @@ def get_example_config() -> Config:
|
|
|
142
143
|
)
|
|
143
144
|
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
def load_config() -> Config | None:
|
|
146
|
+
def _load_config_uncached() -> Config | None:
|
|
147
147
|
if not config_path.exists():
|
|
148
148
|
log(f"Config file not found: {config_path}")
|
|
149
149
|
example_config = get_example_config()
|
|
@@ -151,7 +151,7 @@ def load_config() -> Config | None:
|
|
|
151
151
|
config_dict = example_config.model_dump(mode="json", exclude_none=True)
|
|
152
152
|
|
|
153
153
|
# Comment out all example config lines
|
|
154
|
-
yaml_str = yaml.dump(config_dict, default_flow_style=False, sort_keys=False)
|
|
154
|
+
yaml_str = yaml.dump(config_dict, default_flow_style=False, sort_keys=False) or ""
|
|
155
155
|
commented_yaml = "\n".join(f"# {line}" if line.strip() else "#" for line in yaml_str.splitlines())
|
|
156
156
|
_ = config_path.write_text(commented_yaml)
|
|
157
157
|
|
|
@@ -175,3 +175,30 @@ def load_config() -> Config | None:
|
|
|
175
175
|
raise ValueError(f"Invalid config file: {config_path}") from e
|
|
176
176
|
|
|
177
177
|
return config
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@lru_cache(maxsize=1)
|
|
181
|
+
def _load_config_cached() -> Config | None:
|
|
182
|
+
return _load_config_uncached()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def load_config() -> Config | None:
|
|
186
|
+
"""Load config from disk, caching only successful parses.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Config object on success, or None when the config is missing/empty/commented out.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
config = _load_config_cached()
|
|
194
|
+
except ValueError:
|
|
195
|
+
_load_config_cached.cache_clear()
|
|
196
|
+
raise
|
|
197
|
+
|
|
198
|
+
if config is None:
|
|
199
|
+
_load_config_cached.cache_clear()
|
|
200
|
+
return config
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Expose cache control for tests and callers that need to invalidate the cache.
|
|
204
|
+
load_config.cache_clear = _load_config_cached.cache_clear # type: ignore[attr-defined]
|
klaude_code/const/__init__.py
CHANGED
|
@@ -4,6 +4,8 @@ This module consolidates all magic numbers and configuration values
|
|
|
4
4
|
that were previously scattered across the codebase.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
7
9
|
# =============================================================================
|
|
8
10
|
# Agent Configuration
|
|
9
11
|
# =============================================================================
|
|
@@ -116,15 +118,18 @@ STATUS_SHIMMER_ALPHA_SCALE = 0.7
|
|
|
116
118
|
# Spinner breathing animation
|
|
117
119
|
# Duration in seconds for one full breathe-in + breathe-out cycle
|
|
118
120
|
# Keep in sync with STATUS_SHIMMER_SWEEP_SECONDS for visual consistency
|
|
119
|
-
SPINNER_BREATH_PERIOD_SECONDS = 2
|
|
121
|
+
SPINNER_BREATH_PERIOD_SECONDS: float = 2.0
|
|
120
122
|
|
|
121
123
|
|
|
122
124
|
# =============================================================================
|
|
123
125
|
# Debug / Logging
|
|
124
126
|
# =============================================================================
|
|
125
127
|
|
|
126
|
-
# Default debug log
|
|
127
|
-
|
|
128
|
+
# Default debug log directory (user cache)
|
|
129
|
+
DEFAULT_DEBUG_LOG_DIR = Path.home() / ".klaude" / "logs"
|
|
130
|
+
|
|
131
|
+
# Default debug log file path (symlink to latest session)
|
|
132
|
+
DEFAULT_DEBUG_LOG_FILE = DEFAULT_DEBUG_LOG_DIR / "debug.log"
|
|
128
133
|
|
|
129
134
|
# Maximum log file size before rotation (10MB)
|
|
130
135
|
LOG_MAX_BYTES = 10 * 1024 * 1024
|
klaude_code/core/agent.py
CHANGED
|
@@ -46,7 +46,7 @@ class DefaultModelProfileProvider(ModelProfileProvider):
|
|
|
46
46
|
model_name = llm_client.model_name
|
|
47
47
|
return AgentProfile(
|
|
48
48
|
llm_client=llm_client,
|
|
49
|
-
system_prompt=load_system_prompt(model_name, sub_agent_type),
|
|
49
|
+
system_prompt=load_system_prompt(model_name, llm_client.protocol, sub_agent_type),
|
|
50
50
|
tools=load_agent_tools(model_name, sub_agent_type),
|
|
51
51
|
reminders=load_agent_reminders(model_name, sub_agent_type),
|
|
52
52
|
)
|