klaude-code 1.2.6__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/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
klaude_code/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Package init for CLI; intentionally empty.
|
klaude_code/cli/main.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import uuid
|
|
6
|
+
from importlib.metadata import version as pkg_version
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from klaude_code.cli.runtime import DEBUG_FILTER_HELP, AppInitConfig, resolve_debug_settings, run_exec, run_interactive
|
|
11
|
+
from klaude_code.cli.session_cmd import register_session_commands
|
|
12
|
+
from klaude_code.config import config_path, display_models_and_providers, load_config, select_model_from_config
|
|
13
|
+
from klaude_code.session import Session, resume_select_session
|
|
14
|
+
from klaude_code.trace import log
|
|
15
|
+
from klaude_code.ui.terminal.color import is_light_terminal_background
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_terminal_title(title: str) -> None:
|
|
19
|
+
"""Set terminal window title using ANSI escape sequence."""
|
|
20
|
+
sys.stdout.write(f"\033]0;{title}\007")
|
|
21
|
+
sys.stdout.flush()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _version_callback(value: bool) -> None:
|
|
25
|
+
"""Show version and exit."""
|
|
26
|
+
if value:
|
|
27
|
+
try:
|
|
28
|
+
ver = pkg_version("klaude-code")
|
|
29
|
+
except Exception:
|
|
30
|
+
ver = "unknown"
|
|
31
|
+
print(f"klaude-code {ver}")
|
|
32
|
+
raise typer.Exit(0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
app = typer.Typer(
|
|
36
|
+
add_completion=False,
|
|
37
|
+
pretty_exceptions_enable=False,
|
|
38
|
+
no_args_is_help=False,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
session_app = typer.Typer(help="Manage sessions for the current project")
|
|
42
|
+
register_session_commands(session_app)
|
|
43
|
+
app.add_typer(session_app, name="session")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command("list")
|
|
47
|
+
def list_models() -> None:
|
|
48
|
+
"""List all models and providers configuration"""
|
|
49
|
+
config = load_config()
|
|
50
|
+
if config is None:
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
|
|
53
|
+
# Auto-detect theme when not explicitly set in config, to match other CLI entrypoints.
|
|
54
|
+
if config.theme is None:
|
|
55
|
+
detected = is_light_terminal_background()
|
|
56
|
+
if detected is True:
|
|
57
|
+
config.theme = "light"
|
|
58
|
+
elif detected is False:
|
|
59
|
+
config.theme = "dark"
|
|
60
|
+
|
|
61
|
+
display_models_and_providers(config)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command("config")
|
|
65
|
+
@app.command("conf", hidden=True)
|
|
66
|
+
def edit_config() -> None:
|
|
67
|
+
"""Open the configuration file in $EDITOR or default system editor"""
|
|
68
|
+
editor = os.environ.get("EDITOR")
|
|
69
|
+
|
|
70
|
+
# If no EDITOR is set, prioritize TextEdit on macOS
|
|
71
|
+
if not editor:
|
|
72
|
+
# Try common editors in order of preference on other platforms
|
|
73
|
+
for cmd in [
|
|
74
|
+
"code",
|
|
75
|
+
"nvim",
|
|
76
|
+
"vim",
|
|
77
|
+
"nano",
|
|
78
|
+
]:
|
|
79
|
+
try:
|
|
80
|
+
subprocess.run(["which", cmd], check=True, capture_output=True)
|
|
81
|
+
editor = cmd
|
|
82
|
+
break
|
|
83
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# If no editor found, try platform-specific defaults
|
|
87
|
+
if not editor:
|
|
88
|
+
if sys.platform == "darwin": # macOS
|
|
89
|
+
editor = "open"
|
|
90
|
+
elif sys.platform == "win32": # Windows
|
|
91
|
+
editor = "notepad"
|
|
92
|
+
else: # Linux and other Unix systems
|
|
93
|
+
editor = "xdg-open"
|
|
94
|
+
|
|
95
|
+
# Ensure config file exists
|
|
96
|
+
config = load_config()
|
|
97
|
+
if config is None:
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
if editor == "open -a TextEdit":
|
|
102
|
+
subprocess.run(["open", "-a", "TextEdit", str(config_path)], check=True)
|
|
103
|
+
elif editor in ["open", "xdg-open"]:
|
|
104
|
+
# For open/xdg-open, we need to pass the file directly
|
|
105
|
+
subprocess.run([editor, str(config_path)], check=True)
|
|
106
|
+
else:
|
|
107
|
+
subprocess.run([editor, str(config_path)], check=True)
|
|
108
|
+
except subprocess.CalledProcessError as e:
|
|
109
|
+
log((f"Error: Failed to open editor: {e}", "red"))
|
|
110
|
+
raise typer.Exit(1)
|
|
111
|
+
except FileNotFoundError:
|
|
112
|
+
log((f"Error: Editor '{editor}' not found", "red"))
|
|
113
|
+
log("Please install a text editor or set your $EDITOR environment variable")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.command("exec")
|
|
118
|
+
def exec_command(
|
|
119
|
+
input_content: str = typer.Argument("", help="Input message to execute"),
|
|
120
|
+
model: str | None = typer.Option(
|
|
121
|
+
None,
|
|
122
|
+
"--model",
|
|
123
|
+
"-m",
|
|
124
|
+
help="Override model config name (uses main model by default)",
|
|
125
|
+
rich_help_panel="LLM",
|
|
126
|
+
),
|
|
127
|
+
select_model: bool = typer.Option(
|
|
128
|
+
False,
|
|
129
|
+
"--select-model",
|
|
130
|
+
"-s",
|
|
131
|
+
help="Interactively choose a model at startup",
|
|
132
|
+
rich_help_panel="LLM",
|
|
133
|
+
),
|
|
134
|
+
debug: bool = typer.Option(
|
|
135
|
+
False,
|
|
136
|
+
"--debug",
|
|
137
|
+
"-d",
|
|
138
|
+
help="Enable debug mode",
|
|
139
|
+
rich_help_panel="Debug",
|
|
140
|
+
),
|
|
141
|
+
debug_filter: str | None = typer.Option(
|
|
142
|
+
None,
|
|
143
|
+
"--debug-filter",
|
|
144
|
+
help=DEBUG_FILTER_HELP,
|
|
145
|
+
rich_help_panel="Debug",
|
|
146
|
+
),
|
|
147
|
+
vanilla: bool = typer.Option(
|
|
148
|
+
False,
|
|
149
|
+
"--vanilla",
|
|
150
|
+
help="Vanilla mode exposes the model's raw API behavior: it provides only minimal tools (Bash, Read & Edit) and omits system prompts and reminders.",
|
|
151
|
+
),
|
|
152
|
+
stream_json: bool = typer.Option(
|
|
153
|
+
False,
|
|
154
|
+
"--stream-json",
|
|
155
|
+
help="Stream all events as JSON lines to stdout.",
|
|
156
|
+
),
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Execute non-interactively with provided input."""
|
|
159
|
+
|
|
160
|
+
# Set terminal title with current folder name
|
|
161
|
+
folder_name = os.path.basename(os.getcwd())
|
|
162
|
+
set_terminal_title(f"{folder_name}: klaude")
|
|
163
|
+
|
|
164
|
+
parts: list[str] = []
|
|
165
|
+
|
|
166
|
+
# Handle stdin input
|
|
167
|
+
if not sys.stdin.isatty():
|
|
168
|
+
try:
|
|
169
|
+
stdin = sys.stdin.read().rstrip("\n")
|
|
170
|
+
if stdin:
|
|
171
|
+
parts.append(stdin)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
log((f"Error reading from stdin: {e}", "red"))
|
|
174
|
+
|
|
175
|
+
if input_content:
|
|
176
|
+
parts.append(input_content)
|
|
177
|
+
|
|
178
|
+
input_content = "\n".join(parts)
|
|
179
|
+
if len(input_content) == 0:
|
|
180
|
+
log(("Error: No input content provided", "red"))
|
|
181
|
+
raise typer.Exit(1)
|
|
182
|
+
|
|
183
|
+
chosen_model = model
|
|
184
|
+
if select_model:
|
|
185
|
+
# Prefer the explicitly provided model as default; otherwise main model
|
|
186
|
+
config = load_config()
|
|
187
|
+
if config is None:
|
|
188
|
+
raise typer.Exit(1)
|
|
189
|
+
default_name = model or config.main_model
|
|
190
|
+
chosen_model = select_model_from_config(preferred=default_name)
|
|
191
|
+
if chosen_model is None:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
|
|
195
|
+
|
|
196
|
+
init_config = AppInitConfig(
|
|
197
|
+
model=chosen_model,
|
|
198
|
+
debug=debug_enabled,
|
|
199
|
+
vanilla=vanilla,
|
|
200
|
+
is_exec_mode=True,
|
|
201
|
+
debug_filters=debug_filters,
|
|
202
|
+
stream_json=stream_json,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
asyncio.run(
|
|
206
|
+
run_exec(
|
|
207
|
+
init_config=init_config,
|
|
208
|
+
input_content=input_content,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@app.callback(invoke_without_command=True)
|
|
214
|
+
def main_callback(
|
|
215
|
+
ctx: typer.Context,
|
|
216
|
+
version: bool = typer.Option(
|
|
217
|
+
False,
|
|
218
|
+
"--version",
|
|
219
|
+
"-V",
|
|
220
|
+
help="Show version and exit",
|
|
221
|
+
callback=_version_callback,
|
|
222
|
+
is_eager=True,
|
|
223
|
+
),
|
|
224
|
+
model: str | None = typer.Option(
|
|
225
|
+
None,
|
|
226
|
+
"--model",
|
|
227
|
+
"-m",
|
|
228
|
+
help="Override model config name (uses main model by default)",
|
|
229
|
+
rich_help_panel="LLM",
|
|
230
|
+
),
|
|
231
|
+
continue_: bool = typer.Option(False, "--continue", "-c", help="Continue from latest session"),
|
|
232
|
+
resume: bool = typer.Option(False, "--resume", "-r", help="Select a session to resume for this project"),
|
|
233
|
+
select_model: bool = typer.Option(
|
|
234
|
+
False,
|
|
235
|
+
"--select-model",
|
|
236
|
+
"-s",
|
|
237
|
+
help="Interactively choose a model at startup",
|
|
238
|
+
rich_help_panel="LLM",
|
|
239
|
+
),
|
|
240
|
+
debug: bool = typer.Option(
|
|
241
|
+
False,
|
|
242
|
+
"--debug",
|
|
243
|
+
"-d",
|
|
244
|
+
help="Enable debug mode",
|
|
245
|
+
rich_help_panel="Debug",
|
|
246
|
+
),
|
|
247
|
+
debug_filter: str | None = typer.Option(
|
|
248
|
+
None,
|
|
249
|
+
"--debug-filter",
|
|
250
|
+
help=DEBUG_FILTER_HELP,
|
|
251
|
+
rich_help_panel="Debug",
|
|
252
|
+
),
|
|
253
|
+
vanilla: bool = typer.Option(
|
|
254
|
+
False,
|
|
255
|
+
"--vanilla",
|
|
256
|
+
help="Vanilla mode exposes the model's raw API behavior: it provides only minimal tools (Bash, Read & Edit) and omits system prompts and reminders.",
|
|
257
|
+
),
|
|
258
|
+
) -> None:
|
|
259
|
+
# Only run interactive mode when no subcommand is invoked
|
|
260
|
+
if ctx.invoked_subcommand is None:
|
|
261
|
+
# Set terminal title with current folder name
|
|
262
|
+
folder_name = os.path.basename(os.getcwd())
|
|
263
|
+
set_terminal_title(f"{folder_name}: klaude")
|
|
264
|
+
# Interactive mode
|
|
265
|
+
chosen_model = model
|
|
266
|
+
if select_model:
|
|
267
|
+
chosen_model = select_model_from_config(preferred=model)
|
|
268
|
+
if chosen_model is None:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
# Resolve session id before entering asyncio loop
|
|
272
|
+
session_id: str | None = None
|
|
273
|
+
if resume:
|
|
274
|
+
session_id = resume_select_session()
|
|
275
|
+
if session_id is None:
|
|
276
|
+
return
|
|
277
|
+
# If user didn't pick, allow fallback to --continue
|
|
278
|
+
if session_id is None and continue_:
|
|
279
|
+
session_id = Session.most_recent_session_id()
|
|
280
|
+
# If still no session_id, generate a new one for a new session
|
|
281
|
+
if session_id is None:
|
|
282
|
+
session_id = uuid.uuid4().hex
|
|
283
|
+
|
|
284
|
+
debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
|
|
285
|
+
|
|
286
|
+
init_config = AppInitConfig(
|
|
287
|
+
model=chosen_model,
|
|
288
|
+
debug=debug_enabled,
|
|
289
|
+
vanilla=vanilla,
|
|
290
|
+
debug_filters=debug_filters,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
asyncio.run(
|
|
294
|
+
run_interactive(
|
|
295
|
+
init_config=init_config,
|
|
296
|
+
session_id=session_id,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Protocol
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from klaude_code import ui
|
|
11
|
+
from klaude_code.command import has_interactive_command
|
|
12
|
+
from klaude_code.config import Config, load_config
|
|
13
|
+
from klaude_code.core.agent import Agent, DefaultModelProfileProvider, VanillaModelProfileProvider
|
|
14
|
+
from klaude_code.core.executor import Executor
|
|
15
|
+
from klaude_code.core.manager import build_llm_clients
|
|
16
|
+
from klaude_code.core.tool import SkillLoader, SkillTool
|
|
17
|
+
from klaude_code.protocol import events, op
|
|
18
|
+
from klaude_code.protocol.model import UserInputPayload
|
|
19
|
+
from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
|
|
20
|
+
from klaude_code.trace import DebugType, log, set_debug_logging
|
|
21
|
+
from klaude_code.ui.modes.repl import build_repl_status_snapshot
|
|
22
|
+
from klaude_code.ui.modes.repl.input_prompt_toolkit import REPLStatusSnapshot
|
|
23
|
+
from klaude_code.ui.terminal.color import is_light_terminal_background
|
|
24
|
+
from klaude_code.ui.terminal.control import install_sigint_double_press_exit, start_esc_interrupt_monitor
|
|
25
|
+
from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
|
|
26
|
+
from klaude_code.version import get_update_message
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PrintCapable(Protocol):
|
|
30
|
+
"""Protocol for objects that can print styled content."""
|
|
31
|
+
|
|
32
|
+
def print(self, *objects: Any, style: Any | None = None, end: str = "\n") -> None: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
DEBUG_FILTER_HELP = "Comma-separated debug types: " + ", ".join(dt.value for dt in DebugType)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_debug_filters(raw: str | None) -> set[DebugType] | None:
|
|
39
|
+
if raw is None:
|
|
40
|
+
return None
|
|
41
|
+
filters: set[DebugType] = set()
|
|
42
|
+
for chunk in raw.split(","):
|
|
43
|
+
normalized = chunk.strip().lower().replace("-", "_")
|
|
44
|
+
if not normalized:
|
|
45
|
+
continue
|
|
46
|
+
try:
|
|
47
|
+
filters.add(DebugType(normalized))
|
|
48
|
+
except ValueError: # pragma: no cover - user input validation
|
|
49
|
+
valid_options = ", ".join(dt.value for dt in DebugType)
|
|
50
|
+
log(
|
|
51
|
+
(
|
|
52
|
+
f"Invalid debug filter '{normalized}'. Valid options: {valid_options}",
|
|
53
|
+
"red",
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
raise typer.Exit(2) from None
|
|
57
|
+
return filters or None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_debug_settings(flag: bool, raw_filters: str | None) -> tuple[bool, set[DebugType] | None]:
|
|
61
|
+
filters = _parse_debug_filters(raw_filters)
|
|
62
|
+
effective_flag = flag or (filters is not None)
|
|
63
|
+
return effective_flag, filters
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class AppInitConfig:
|
|
68
|
+
"""Configuration for initializing the application components."""
|
|
69
|
+
|
|
70
|
+
model: str | None
|
|
71
|
+
debug: bool
|
|
72
|
+
vanilla: bool
|
|
73
|
+
is_exec_mode: bool = False
|
|
74
|
+
debug_filters: set[DebugType] | None = None
|
|
75
|
+
stream_json: bool = False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class AppComponents:
|
|
80
|
+
"""Initialized application components."""
|
|
81
|
+
|
|
82
|
+
config: Config
|
|
83
|
+
executor: Executor
|
|
84
|
+
executor_task: asyncio.Task[None]
|
|
85
|
+
event_queue: asyncio.Queue[events.Event]
|
|
86
|
+
display: ui.DisplayABC
|
|
87
|
+
display_task: asyncio.Task[None]
|
|
88
|
+
theme: str | None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def initialize_app_components(init_config: AppInitConfig) -> AppComponents:
|
|
92
|
+
"""Initialize all application components (LLM clients, executor, UI)."""
|
|
93
|
+
set_debug_logging(init_config.debug, filters=init_config.debug_filters)
|
|
94
|
+
|
|
95
|
+
config = load_config()
|
|
96
|
+
if config is None:
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
|
|
99
|
+
# Initialize skills
|
|
100
|
+
skill_loader = SkillLoader()
|
|
101
|
+
skill_loader.discover_skills()
|
|
102
|
+
SkillTool.set_skill_loader(skill_loader)
|
|
103
|
+
|
|
104
|
+
# Initialize LLM clients
|
|
105
|
+
try:
|
|
106
|
+
enabled_sub_agents = [p.name for p in iter_sub_agent_profiles()]
|
|
107
|
+
llm_clients = build_llm_clients(
|
|
108
|
+
config,
|
|
109
|
+
model_override=init_config.model,
|
|
110
|
+
enabled_sub_agents=enabled_sub_agents,
|
|
111
|
+
)
|
|
112
|
+
except ValueError as exc:
|
|
113
|
+
if init_config.model:
|
|
114
|
+
log(
|
|
115
|
+
(
|
|
116
|
+
f"Error: model '{init_config.model}' is not defined in the config",
|
|
117
|
+
"red",
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
log(("Hint: run `klaude list` to view available models", "yellow"))
|
|
121
|
+
else:
|
|
122
|
+
log((f"Error: failed to load the default model configuration: {exc}", "red"))
|
|
123
|
+
raise typer.Exit(2) from None
|
|
124
|
+
|
|
125
|
+
model_profile_provider = VanillaModelProfileProvider() if init_config.vanilla else DefaultModelProfileProvider()
|
|
126
|
+
|
|
127
|
+
# Create event queue for communication between executor and UI
|
|
128
|
+
event_queue: asyncio.Queue[events.Event] = asyncio.Queue()
|
|
129
|
+
|
|
130
|
+
# Create executor with the LLM client
|
|
131
|
+
executor = Executor(
|
|
132
|
+
event_queue,
|
|
133
|
+
llm_clients,
|
|
134
|
+
model_profile_provider=model_profile_provider,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Start executor in background
|
|
138
|
+
executor_task = asyncio.create_task(executor.start())
|
|
139
|
+
|
|
140
|
+
theme: str | None = config.theme
|
|
141
|
+
if theme is None:
|
|
142
|
+
# Auto-detect theme from terminal background when config does not specify a theme.
|
|
143
|
+
detected = is_light_terminal_background()
|
|
144
|
+
if detected is True:
|
|
145
|
+
theme = "light"
|
|
146
|
+
elif detected is False:
|
|
147
|
+
theme = "dark"
|
|
148
|
+
|
|
149
|
+
# Set up UI components using factory functions
|
|
150
|
+
display: ui.DisplayABC
|
|
151
|
+
if init_config.is_exec_mode:
|
|
152
|
+
display = ui.create_exec_display(debug=init_config.debug, stream_json=init_config.stream_json)
|
|
153
|
+
else:
|
|
154
|
+
display = ui.create_default_display(debug=init_config.debug, theme=theme)
|
|
155
|
+
|
|
156
|
+
# Start UI display task
|
|
157
|
+
display_task = asyncio.create_task(display.consume_event_loop(event_queue))
|
|
158
|
+
|
|
159
|
+
return AppComponents(
|
|
160
|
+
config=config,
|
|
161
|
+
executor=executor,
|
|
162
|
+
executor_task=executor_task,
|
|
163
|
+
event_queue=event_queue,
|
|
164
|
+
display=display,
|
|
165
|
+
display_task=display_task,
|
|
166
|
+
theme=theme,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def cleanup_app_components(components: AppComponents) -> None:
|
|
171
|
+
"""Clean up all application components."""
|
|
172
|
+
try:
|
|
173
|
+
# Clean shutdown
|
|
174
|
+
await components.executor.stop()
|
|
175
|
+
components.executor_task.cancel()
|
|
176
|
+
|
|
177
|
+
# Signal UI to stop
|
|
178
|
+
await components.event_queue.put(events.EndEvent())
|
|
179
|
+
await components.display_task
|
|
180
|
+
finally:
|
|
181
|
+
# Always attempt to clear Ghostty progress bar and restore cursor visibility
|
|
182
|
+
try:
|
|
183
|
+
emit_osc94(OSC94States.HIDDEN)
|
|
184
|
+
except Exception:
|
|
185
|
+
# Best-effort only; never fail cleanup due to OSC errors
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
# Ensure the terminal cursor is visible even if Rich's Status spinner
|
|
190
|
+
# did not get a chance to stop cleanly (e.g. on KeyboardInterrupt).
|
|
191
|
+
stream = getattr(sys, "__stdout__", None) or sys.stdout
|
|
192
|
+
stream.write("\033[?25h")
|
|
193
|
+
stream.flush()
|
|
194
|
+
except Exception:
|
|
195
|
+
# If this fails the shell can still recover via `reset`/`stty sane`.
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def _handle_keyboard_interrupt(executor: Executor) -> None:
|
|
200
|
+
"""Handle Ctrl+C by logging and sending a global interrupt."""
|
|
201
|
+
|
|
202
|
+
log("Bye!")
|
|
203
|
+
try:
|
|
204
|
+
await executor.submit(op.InterruptOperation(target_session_id=None))
|
|
205
|
+
except Exception:
|
|
206
|
+
# Executor might already be stopping
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
|
|
211
|
+
"""Run a single command non-interactively using the provided configuration."""
|
|
212
|
+
|
|
213
|
+
components = await initialize_app_components(init_config)
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Generate a new session ID for exec mode
|
|
217
|
+
session_id = uuid.uuid4().hex
|
|
218
|
+
|
|
219
|
+
# Init Agent
|
|
220
|
+
await components.executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
|
|
221
|
+
await components.event_queue.join()
|
|
222
|
+
|
|
223
|
+
# Submit the input content directly
|
|
224
|
+
await components.executor.submit_and_wait(
|
|
225
|
+
op.UserInputOperation(input=UserInputPayload(text=input_content), session_id=session_id)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
except KeyboardInterrupt:
|
|
229
|
+
await _handle_keyboard_interrupt(components.executor)
|
|
230
|
+
finally:
|
|
231
|
+
await cleanup_app_components(components)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async def run_interactive(init_config: AppInitConfig, session_id: str | None = None) -> None:
|
|
235
|
+
"""Run the interactive REPL using the provided configuration."""
|
|
236
|
+
|
|
237
|
+
components = await initialize_app_components(init_config)
|
|
238
|
+
|
|
239
|
+
# No theme persistence from CLI anymore; config.theme controls theme when set.
|
|
240
|
+
|
|
241
|
+
# Create status provider for bottom toolbar
|
|
242
|
+
def _status_provider() -> REPLStatusSnapshot:
|
|
243
|
+
agent: Agent | None = None
|
|
244
|
+
if session_id and session_id in components.executor.context.active_agents:
|
|
245
|
+
agent = components.executor.context.active_agents[session_id]
|
|
246
|
+
|
|
247
|
+
# Check for updates (returns None if uv not available)
|
|
248
|
+
update_message = get_update_message()
|
|
249
|
+
|
|
250
|
+
return build_repl_status_snapshot(agent=agent, update_message=update_message)
|
|
251
|
+
|
|
252
|
+
# Set up input provider for interactive mode
|
|
253
|
+
input_provider: ui.InputProviderABC = ui.PromptToolkitInput(status_provider=_status_provider)
|
|
254
|
+
|
|
255
|
+
# --- Custom Ctrl+C handler: double-press within 2s to exit, single press shows toast ---
|
|
256
|
+
def _show_toast_once() -> None:
|
|
257
|
+
MSG = "Press ctrl+c again to exit"
|
|
258
|
+
try:
|
|
259
|
+
# Keep message short; avoid interfering with spinner layout
|
|
260
|
+
printer: PrintCapable | None = None
|
|
261
|
+
|
|
262
|
+
# Check if it's a REPLDisplay with renderer
|
|
263
|
+
if isinstance(components.display, ui.REPLDisplay):
|
|
264
|
+
printer = components.display.renderer
|
|
265
|
+
# Check if it's a DebugEventDisplay wrapping a REPLDisplay
|
|
266
|
+
elif isinstance(components.display, ui.DebugEventDisplay) and components.display.wrapped_display:
|
|
267
|
+
if isinstance(components.display.wrapped_display, ui.REPLDisplay):
|
|
268
|
+
printer = components.display.wrapped_display.renderer
|
|
269
|
+
|
|
270
|
+
if printer is not None:
|
|
271
|
+
printer.print(Text(f" {MSG} ", style="bold yellow reverse"))
|
|
272
|
+
else:
|
|
273
|
+
print(MSG, file=sys.stderr)
|
|
274
|
+
except Exception:
|
|
275
|
+
# Fallback if themed print is unavailable
|
|
276
|
+
print(MSG, file=sys.stderr)
|
|
277
|
+
|
|
278
|
+
def _hide_progress() -> None:
|
|
279
|
+
try:
|
|
280
|
+
emit_osc94(OSC94States.HIDDEN)
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
# Init Agent
|
|
288
|
+
await components.executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
|
|
289
|
+
await components.event_queue.join()
|
|
290
|
+
# Input
|
|
291
|
+
await input_provider.start()
|
|
292
|
+
async for user_input in input_provider.iter_inputs():
|
|
293
|
+
# Handle special commands
|
|
294
|
+
if user_input.text.strip().lower() in {"exit", ":q", "quit"}:
|
|
295
|
+
break
|
|
296
|
+
elif user_input.text.strip() == "":
|
|
297
|
+
continue
|
|
298
|
+
# Submit user input operation - directly use the payload from iter_inputs
|
|
299
|
+
submission_id = await components.executor.submit(
|
|
300
|
+
op.UserInputOperation(input=user_input, session_id=session_id)
|
|
301
|
+
)
|
|
302
|
+
# If it's an interactive command (e.g., /model), avoid starting the ESC monitor
|
|
303
|
+
# to prevent TTY conflicts with interactive prompts (questionary/prompt_toolkit).
|
|
304
|
+
if has_interactive_command(user_input.text):
|
|
305
|
+
await components.executor.wait_for(submission_id)
|
|
306
|
+
else:
|
|
307
|
+
# Esc monitor for long-running, interruptible operations
|
|
308
|
+
async def _on_esc_interrupt() -> None:
|
|
309
|
+
await components.executor.submit(op.InterruptOperation(target_session_id=session_id))
|
|
310
|
+
|
|
311
|
+
stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
|
|
312
|
+
# Wait for this specific task to complete before accepting next input
|
|
313
|
+
try:
|
|
314
|
+
await components.executor.wait_for(submission_id)
|
|
315
|
+
finally:
|
|
316
|
+
# Stop ESC monitor and wait for it to finish cleaning up TTY
|
|
317
|
+
stop_event.set()
|
|
318
|
+
try:
|
|
319
|
+
await esc_task
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
except KeyboardInterrupt:
|
|
324
|
+
await _handle_keyboard_interrupt(components.executor)
|
|
325
|
+
finally:
|
|
326
|
+
try:
|
|
327
|
+
# Restore original SIGINT handler
|
|
328
|
+
restore_sigint()
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
await cleanup_app_components(components)
|