klaude-code 2.0.2__py3-none-any.whl → 2.1.1__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/app/__init__.py +12 -0
- klaude_code/app/runtime.py +215 -0
- klaude_code/cli/auth_cmd.py +2 -2
- klaude_code/cli/config_cmd.py +2 -2
- klaude_code/cli/cost_cmd.py +1 -1
- klaude_code/cli/debug.py +12 -36
- klaude_code/cli/list_model.py +3 -3
- klaude_code/cli/main.py +17 -60
- klaude_code/cli/self_update.py +2 -187
- klaude_code/cli/session_cmd.py +2 -2
- klaude_code/config/config.py +1 -1
- klaude_code/config/select_model.py +1 -1
- klaude_code/const.py +9 -1
- klaude_code/core/agent.py +9 -62
- klaude_code/core/agent_profile.py +291 -0
- klaude_code/core/executor.py +335 -230
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +16 -29
- klaude_code/core/reminders.py +84 -103
- klaude_code/core/task.py +12 -20
- klaude_code/core/tool/__init__.py +5 -19
- klaude_code/core/tool/context.py +84 -0
- klaude_code/core/tool/file/apply_patch_tool.py +18 -21
- klaude_code/core/tool/file/edit_tool.py +39 -42
- klaude_code/core/tool/file/read_tool.py +14 -9
- klaude_code/core/tool/file/write_tool.py +12 -13
- klaude_code/core/tool/report_back_tool.py +4 -1
- klaude_code/core/tool/shell/bash_tool.py +6 -11
- klaude_code/core/tool/sub_agent_tool.py +8 -7
- klaude_code/core/tool/todo/todo_write_tool.py +3 -9
- klaude_code/core/tool/todo/update_plan_tool.py +3 -5
- klaude_code/core/tool/tool_abc.py +2 -1
- klaude_code/core/tool/tool_registry.py +2 -33
- klaude_code/core/tool/tool_runner.py +13 -10
- klaude_code/core/tool/web/mermaid_tool.py +3 -1
- klaude_code/core/tool/web/web_fetch_tool.py +5 -3
- klaude_code/core/tool/web/web_search_tool.py +5 -3
- klaude_code/core/turn.py +87 -30
- klaude_code/llm/anthropic/client.py +1 -1
- klaude_code/llm/bedrock/client.py +1 -1
- klaude_code/llm/claude/client.py +1 -1
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +1 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
- klaude_code/llm/openrouter/client.py +1 -1
- klaude_code/llm/openrouter/reasoning.py +1 -1
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events/__init__.py +57 -0
- klaude_code/protocol/events/base.py +18 -0
- klaude_code/protocol/events/chat.py +20 -0
- klaude_code/protocol/events/lifecycle.py +22 -0
- klaude_code/protocol/events/metadata.py +15 -0
- klaude_code/protocol/events/streaming.py +43 -0
- klaude_code/protocol/events/system.py +53 -0
- klaude_code/protocol/events/tools.py +27 -0
- klaude_code/protocol/op.py +5 -0
- klaude_code/protocol/tools.py +0 -1
- klaude_code/session/session.py +6 -7
- klaude_code/skill/assets/create-plan/SKILL.md +76 -0
- klaude_code/skill/loader.py +32 -88
- klaude_code/skill/manager.py +38 -0
- klaude_code/skill/system_skills.py +1 -1
- klaude_code/tui/__init__.py +8 -0
- klaude_code/{command → tui/command}/__init__.py +3 -0
- klaude_code/{command → tui/command}/clear_cmd.py +2 -1
- klaude_code/tui/command/copy_cmd.py +53 -0
- klaude_code/{command → tui/command}/debug_cmd.py +3 -2
- klaude_code/{command → tui/command}/export_cmd.py +2 -1
- klaude_code/{command → tui/command}/export_online_cmd.py +2 -1
- klaude_code/{command → tui/command}/fork_session_cmd.py +4 -3
- klaude_code/{command → tui/command}/help_cmd.py +2 -1
- klaude_code/{command → tui/command}/model_cmd.py +4 -3
- klaude_code/{command → tui/command}/model_select.py +2 -2
- klaude_code/{command → tui/command}/prompt_command.py +4 -3
- klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
- klaude_code/{command → tui/command}/registry.py +6 -5
- klaude_code/{command → tui/command}/release_notes_cmd.py +2 -1
- klaude_code/{command → tui/command}/resume_cmd.py +4 -3
- klaude_code/{command → tui/command}/status_cmd.py +2 -1
- klaude_code/{command → tui/command}/terminal_setup_cmd.py +2 -1
- klaude_code/{command → tui/command}/thinking_cmd.py +3 -2
- klaude_code/tui/commands.py +164 -0
- klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
- klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
- klaude_code/{ui/renderers → tui/components}/common.py +1 -1
- klaude_code/{ui/renderers → tui/components}/developer.py +4 -4
- klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
- klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
- klaude_code/{ui/renderers → tui/components}/metadata.py +7 -7
- klaude_code/{ui → tui/components}/rich/markdown.py +9 -23
- klaude_code/{ui → tui/components}/rich/status.py +2 -2
- klaude_code/{ui → tui/components}/rich/theme.py +3 -1
- klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
- klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
- klaude_code/{ui/renderers → tui/components}/tools.py +13 -17
- klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
- klaude_code/tui/display.py +85 -0
- klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
- klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
- klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +6 -6
- klaude_code/tui/machine.py +608 -0
- klaude_code/tui/renderer.py +707 -0
- klaude_code/tui/runner.py +321 -0
- klaude_code/tui/terminal/__init__.py +56 -0
- klaude_code/{ui → tui}/terminal/color.py +1 -1
- klaude_code/{ui → tui}/terminal/control.py +1 -1
- klaude_code/{ui → tui}/terminal/notifier.py +1 -1
- klaude_code/ui/__init__.py +6 -50
- klaude_code/ui/core/display.py +3 -3
- klaude_code/ui/core/input.py +2 -1
- klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
- klaude_code/ui/{modes/exec/display.py → exec_mode.py} +0 -2
- klaude_code/ui/terminal/__init__.py +6 -54
- klaude_code/ui/terminal/title.py +31 -0
- klaude_code/update.py +163 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/METADATA +1 -1
- klaude_code-2.1.1.dist-info/RECORD +233 -0
- klaude_code/cli/runtime.py +0 -518
- klaude_code/core/prompt.py +0 -108
- klaude_code/core/tool/skill/skill_tool.md +0 -24
- klaude_code/core/tool/skill/skill_tool.py +0 -87
- klaude_code/core/tool/tool_context.py +0 -148
- klaude_code/protocol/events.py +0 -195
- klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
- klaude_code/trace/__init__.py +0 -21
- klaude_code/ui/core/stage_manager.py +0 -48
- klaude_code/ui/modes/__init__.py +0 -1
- klaude_code/ui/modes/debug/__init__.py +0 -1
- klaude_code/ui/modes/exec/__init__.py +0 -1
- klaude_code/ui/modes/repl/display.py +0 -61
- klaude_code/ui/modes/repl/event_handler.py +0 -629
- klaude_code/ui/modes/repl/renderer.py +0 -464
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/utils/__init__.py +0 -1
- klaude_code-2.0.2.dist-info/RECORD +0 -227
- /klaude_code/{trace/log.py → log.py} +0 -0
- /klaude_code/{command → tui/command}/command_abc.py +0 -0
- /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
- /klaude_code/{command → tui/command}/prompt-init.md +0 -0
- /klaude_code/{core/tool/skill → tui/components}/__init__.py +0 -0
- /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
- /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
- /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
- /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
- /klaude_code/{ui → tui/components}/rich/live.py +0 -0
- /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
- /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
- /klaude_code/{ui → tui}/terminal/image.py +0 -0
- /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
- /klaude_code/{ui → tui}/terminal/selector.py +0 -0
- /klaude_code/ui/{utils/common.py → common.py} +0 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/WHEEL +0 -0
- {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/entry_points.txt +0 -0
klaude_code/cli/runtime.py
DELETED
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import contextlib
|
|
3
|
-
import sys
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from typing import Any, Protocol
|
|
6
|
-
from uuid import uuid4
|
|
7
|
-
|
|
8
|
-
import typer
|
|
9
|
-
from rich.text import Text
|
|
10
|
-
|
|
11
|
-
from klaude_code import ui
|
|
12
|
-
from klaude_code.cli.main import update_terminal_title
|
|
13
|
-
from klaude_code.cli.self_update import get_update_message
|
|
14
|
-
from klaude_code.command import dispatch_command, get_command_info_list, has_interactive_command, is_slash_command_name
|
|
15
|
-
from klaude_code.config import Config, load_config
|
|
16
|
-
from klaude_code.core.agent import Agent, DefaultModelProfileProvider, VanillaModelProfileProvider
|
|
17
|
-
from klaude_code.core.executor import Executor
|
|
18
|
-
from klaude_code.core.manager import build_llm_clients
|
|
19
|
-
from klaude_code.protocol import events, llm_param, op
|
|
20
|
-
from klaude_code.protocol import message as protocol_message
|
|
21
|
-
from klaude_code.protocol.message import UserInputPayload
|
|
22
|
-
from klaude_code.session.session import Session, close_default_store
|
|
23
|
-
from klaude_code.trace import DebugType, log, set_debug_logging
|
|
24
|
-
from klaude_code.ui.modes.repl import build_repl_status_snapshot
|
|
25
|
-
from klaude_code.ui.modes.repl.input_prompt_toolkit import REPLStatusSnapshot
|
|
26
|
-
from klaude_code.ui.terminal.color import is_light_terminal_background
|
|
27
|
-
from klaude_code.ui.terminal.control import install_sigint_double_press_exit, start_esc_interrupt_monitor
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class PrintCapable(Protocol):
|
|
31
|
-
"""Protocol for objects that can print styled content."""
|
|
32
|
-
|
|
33
|
-
def print(self, *objects: Any, style: Any | None = None, end: str = "\n") -> None: ...
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclass
|
|
37
|
-
class AppInitConfig:
|
|
38
|
-
"""Configuration for initializing the application components."""
|
|
39
|
-
|
|
40
|
-
model: str | None
|
|
41
|
-
debug: bool
|
|
42
|
-
vanilla: bool
|
|
43
|
-
is_exec_mode: bool = False
|
|
44
|
-
debug_filters: set[DebugType] | None = None
|
|
45
|
-
stream_json: bool = False
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclass
|
|
49
|
-
class AppComponents:
|
|
50
|
-
"""Initialized application components."""
|
|
51
|
-
|
|
52
|
-
config: Config
|
|
53
|
-
executor: Executor
|
|
54
|
-
executor_task: asyncio.Task[None]
|
|
55
|
-
event_queue: asyncio.Queue[events.Event]
|
|
56
|
-
display: ui.DisplayABC
|
|
57
|
-
display_task: asyncio.Task[None]
|
|
58
|
-
theme: str | None
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
async def submit_user_input_payload(
|
|
62
|
-
*,
|
|
63
|
-
executor: Executor,
|
|
64
|
-
event_queue: asyncio.Queue[events.Event],
|
|
65
|
-
user_input: UserInputPayload,
|
|
66
|
-
session_id: str | None,
|
|
67
|
-
) -> str | None:
|
|
68
|
-
"""Parse/dispatch a user input payload and submit resulting operations.
|
|
69
|
-
|
|
70
|
-
The UI/CLI layer owns slash command parsing and any interactive prompts.
|
|
71
|
-
Core only executes concrete operations.
|
|
72
|
-
|
|
73
|
-
Returns a submission id that should be awaited, or None if there is nothing
|
|
74
|
-
to wait for (e.g. commands that only emit events).
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
sid = session_id or executor.context.current_session_id()
|
|
78
|
-
if sid is None:
|
|
79
|
-
raise RuntimeError("No active session")
|
|
80
|
-
|
|
81
|
-
agent = executor.context.current_agent
|
|
82
|
-
if agent is None or agent.session.id != sid:
|
|
83
|
-
await executor.submit_and_wait(op.InitAgentOperation(session_id=sid))
|
|
84
|
-
agent = executor.context.current_agent
|
|
85
|
-
|
|
86
|
-
if agent is None:
|
|
87
|
-
raise RuntimeError("Failed to initialize agent")
|
|
88
|
-
|
|
89
|
-
submission_id = uuid4().hex
|
|
90
|
-
|
|
91
|
-
await executor.context.emit_event(
|
|
92
|
-
events.UserMessageEvent(content=user_input.text, session_id=sid, images=user_input.images)
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
result = await dispatch_command(user_input, agent, submission_id=submission_id)
|
|
96
|
-
operations: list[op.Operation] = list(result.operations or [])
|
|
97
|
-
|
|
98
|
-
run_ops = [candidate for candidate in operations if isinstance(candidate, op.RunAgentOperation)]
|
|
99
|
-
if len(run_ops) > 1:
|
|
100
|
-
raise ValueError("Multiple RunAgentOperation results are not supported")
|
|
101
|
-
|
|
102
|
-
persisted_user_input = run_ops[0].input if run_ops else user_input
|
|
103
|
-
|
|
104
|
-
if result.persist_user_input:
|
|
105
|
-
agent.session.append_history(
|
|
106
|
-
[
|
|
107
|
-
protocol_message.UserMessage(
|
|
108
|
-
parts=protocol_message.parts_from_text_and_images(
|
|
109
|
-
persisted_user_input.text,
|
|
110
|
-
persisted_user_input.images,
|
|
111
|
-
)
|
|
112
|
-
)
|
|
113
|
-
]
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
if result.events:
|
|
117
|
-
for evt in result.events:
|
|
118
|
-
if result.persist_events and isinstance(evt, events.DeveloperMessageEvent):
|
|
119
|
-
agent.session.append_history([evt.item])
|
|
120
|
-
await executor.context.emit_event(evt)
|
|
121
|
-
|
|
122
|
-
submitted_ids: list[str] = []
|
|
123
|
-
for operation_item in operations:
|
|
124
|
-
submitted_ids.append(await executor.submit(operation_item))
|
|
125
|
-
|
|
126
|
-
if not submitted_ids:
|
|
127
|
-
# Ensure event-only commands are fully rendered before showing the next prompt.
|
|
128
|
-
await event_queue.join()
|
|
129
|
-
return None
|
|
130
|
-
|
|
131
|
-
if run_ops:
|
|
132
|
-
return run_ops[0].id
|
|
133
|
-
return submitted_ids[-1]
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
async def initialize_app_components(init_config: AppInitConfig) -> AppComponents:
|
|
137
|
-
"""Initialize all application components (LLM clients, executor, UI)."""
|
|
138
|
-
set_debug_logging(init_config.debug, filters=init_config.debug_filters)
|
|
139
|
-
|
|
140
|
-
config = load_config()
|
|
141
|
-
|
|
142
|
-
# Initialize LLM clients
|
|
143
|
-
try:
|
|
144
|
-
llm_clients = build_llm_clients(
|
|
145
|
-
config,
|
|
146
|
-
model_override=init_config.model,
|
|
147
|
-
)
|
|
148
|
-
except ValueError as exc:
|
|
149
|
-
if init_config.model:
|
|
150
|
-
log(
|
|
151
|
-
(
|
|
152
|
-
f"Error: model '{init_config.model}' is not defined in the config",
|
|
153
|
-
"red",
|
|
154
|
-
)
|
|
155
|
-
)
|
|
156
|
-
log(("Hint: run `klaude list` to view available models", "yellow"))
|
|
157
|
-
else:
|
|
158
|
-
log((f"Error: failed to load the default model configuration: {exc}", "red"))
|
|
159
|
-
raise typer.Exit(2) from None
|
|
160
|
-
|
|
161
|
-
model_profile_provider = VanillaModelProfileProvider() if init_config.vanilla else DefaultModelProfileProvider()
|
|
162
|
-
|
|
163
|
-
# Create event queue for communication between executor and UI
|
|
164
|
-
event_queue: asyncio.Queue[events.Event] = asyncio.Queue()
|
|
165
|
-
|
|
166
|
-
# Create executor with the LLM client
|
|
167
|
-
executor = Executor(
|
|
168
|
-
event_queue,
|
|
169
|
-
llm_clients,
|
|
170
|
-
model_profile_provider=model_profile_provider,
|
|
171
|
-
on_model_change=update_terminal_title,
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
# Update terminal title with initial model name
|
|
175
|
-
update_terminal_title(llm_clients.main.model_name)
|
|
176
|
-
|
|
177
|
-
# Start executor in background
|
|
178
|
-
executor_task = asyncio.create_task(executor.start())
|
|
179
|
-
|
|
180
|
-
theme: str | None = config.theme
|
|
181
|
-
if theme is None and not init_config.is_exec_mode:
|
|
182
|
-
# Auto-detect theme from terminal background when config does not specify a theme.
|
|
183
|
-
# Skip detection in exec mode to avoid TTY race conditions with parent process's
|
|
184
|
-
# ESC monitor when running as a subprocess.
|
|
185
|
-
detected = is_light_terminal_background()
|
|
186
|
-
if detected is True:
|
|
187
|
-
theme = "light"
|
|
188
|
-
elif detected is False:
|
|
189
|
-
theme = "dark"
|
|
190
|
-
|
|
191
|
-
# Set up UI components using factory functions
|
|
192
|
-
display: ui.DisplayABC
|
|
193
|
-
if init_config.is_exec_mode:
|
|
194
|
-
display = ui.create_exec_display(debug=init_config.debug, stream_json=init_config.stream_json)
|
|
195
|
-
else:
|
|
196
|
-
display = ui.create_default_display(debug=init_config.debug, theme=theme)
|
|
197
|
-
|
|
198
|
-
# Start UI display task
|
|
199
|
-
display_task = asyncio.create_task(display.consume_event_loop(event_queue))
|
|
200
|
-
|
|
201
|
-
return AppComponents(
|
|
202
|
-
config=config,
|
|
203
|
-
executor=executor,
|
|
204
|
-
executor_task=executor_task,
|
|
205
|
-
event_queue=event_queue,
|
|
206
|
-
display=display,
|
|
207
|
-
display_task=display_task,
|
|
208
|
-
theme=theme,
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
async def initialize_session(
|
|
213
|
-
executor: Executor,
|
|
214
|
-
event_queue: asyncio.Queue[events.Event],
|
|
215
|
-
session_id: str | None = None,
|
|
216
|
-
) -> str | None:
|
|
217
|
-
"""Initialize a session and return the active session ID.
|
|
218
|
-
|
|
219
|
-
Args:
|
|
220
|
-
executor: The executor to submit operations to.
|
|
221
|
-
event_queue: The event queue for synchronization.
|
|
222
|
-
session_id: Optional session ID to resume. If None, creates a new session.
|
|
223
|
-
|
|
224
|
-
Returns:
|
|
225
|
-
The active session ID, or None if no session is active.
|
|
226
|
-
"""
|
|
227
|
-
await executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
|
|
228
|
-
await event_queue.join()
|
|
229
|
-
|
|
230
|
-
active_session_id = executor.context.current_session_id()
|
|
231
|
-
return active_session_id or session_id
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def _backfill_session_model_config(
|
|
235
|
-
agent: Agent | None,
|
|
236
|
-
model_override: str | None,
|
|
237
|
-
default_model: str | None,
|
|
238
|
-
is_new_session: bool,
|
|
239
|
-
) -> None:
|
|
240
|
-
"""Backfill model_config_name and model_thinking on newly created sessions."""
|
|
241
|
-
if agent is None or agent.session.model_config_name is not None:
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
if model_override is not None:
|
|
245
|
-
agent.session.model_config_name = model_override
|
|
246
|
-
elif is_new_session and default_model is not None:
|
|
247
|
-
agent.session.model_config_name = default_model
|
|
248
|
-
else:
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
if agent.session.model_thinking is None and agent.profile:
|
|
252
|
-
agent.session.model_thinking = agent.profile.llm_client.get_llm_config().thinking
|
|
253
|
-
# Don't save here - session will be saved when first message is sent via append_history()
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
async def cleanup_app_components(components: AppComponents) -> None:
|
|
257
|
-
"""Clean up all application components."""
|
|
258
|
-
try:
|
|
259
|
-
# Clean shutdown
|
|
260
|
-
await components.executor.stop()
|
|
261
|
-
components.executor_task.cancel()
|
|
262
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
263
|
-
await components.executor_task
|
|
264
|
-
with contextlib.suppress(Exception):
|
|
265
|
-
await close_default_store()
|
|
266
|
-
|
|
267
|
-
# Signal UI to stop
|
|
268
|
-
await components.event_queue.put(events.EndEvent())
|
|
269
|
-
await components.display_task
|
|
270
|
-
finally:
|
|
271
|
-
# Ensure the terminal cursor is visible even if Rich's Status spinner
|
|
272
|
-
# did not get a chance to stop cleanly (e.g. on KeyboardInterrupt).
|
|
273
|
-
# If this fails the shell can still recover via `reset`/`stty sane`.
|
|
274
|
-
with contextlib.suppress(Exception):
|
|
275
|
-
stream = getattr(sys, "__stdout__", None) or sys.stdout
|
|
276
|
-
stream.write("\033[?25h")
|
|
277
|
-
stream.flush()
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
async def _handle_keyboard_interrupt(executor: Executor) -> None:
|
|
281
|
-
"""Handle Ctrl+C by logging and sending a global interrupt."""
|
|
282
|
-
|
|
283
|
-
log("Bye!")
|
|
284
|
-
session_id = executor.context.current_session_id()
|
|
285
|
-
if session_id and Session.exists(session_id):
|
|
286
|
-
log(("Resume with:", "dim"), (f"klaude --resume-by-id {session_id}", "green"))
|
|
287
|
-
# Executor might already be stopping
|
|
288
|
-
with contextlib.suppress(Exception):
|
|
289
|
-
await executor.submit(op.InterruptOperation(target_session_id=None))
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
|
|
293
|
-
"""Run a single command non-interactively using the provided configuration."""
|
|
294
|
-
|
|
295
|
-
components = await initialize_app_components(init_config)
|
|
296
|
-
|
|
297
|
-
try:
|
|
298
|
-
session_id = await initialize_session(components.executor, components.event_queue)
|
|
299
|
-
_backfill_session_model_config(
|
|
300
|
-
components.executor.context.current_agent,
|
|
301
|
-
init_config.model,
|
|
302
|
-
components.config.main_model,
|
|
303
|
-
is_new_session=True,
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
wait_id = await submit_user_input_payload(
|
|
307
|
-
executor=components.executor,
|
|
308
|
-
event_queue=components.event_queue,
|
|
309
|
-
user_input=UserInputPayload(text=input_content),
|
|
310
|
-
session_id=session_id,
|
|
311
|
-
)
|
|
312
|
-
if wait_id is not None:
|
|
313
|
-
await components.executor.wait_for(wait_id)
|
|
314
|
-
await components.event_queue.join()
|
|
315
|
-
|
|
316
|
-
except KeyboardInterrupt:
|
|
317
|
-
await _handle_keyboard_interrupt(components.executor)
|
|
318
|
-
finally:
|
|
319
|
-
await cleanup_app_components(components)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
async def run_interactive(init_config: AppInitConfig, session_id: str | None = None) -> None:
|
|
323
|
-
"""Run the interactive REPL using the provided configuration.
|
|
324
|
-
|
|
325
|
-
If session_id is None, a new session is created with an auto-generated ID.
|
|
326
|
-
If session_id is provided, attempts to resume that session.
|
|
327
|
-
"""
|
|
328
|
-
components = await initialize_app_components(init_config)
|
|
329
|
-
|
|
330
|
-
# No theme persistence from CLI anymore; config.theme controls theme when set.
|
|
331
|
-
|
|
332
|
-
# Create status provider for bottom toolbar
|
|
333
|
-
def _status_provider() -> REPLStatusSnapshot:
|
|
334
|
-
update_message = get_update_message()
|
|
335
|
-
return build_repl_status_snapshot(update_message)
|
|
336
|
-
|
|
337
|
-
# Set up input provider for interactive mode
|
|
338
|
-
def _stop_rich_bottom_ui() -> None:
|
|
339
|
-
display = components.display
|
|
340
|
-
if isinstance(display, ui.REPLDisplay):
|
|
341
|
-
display.renderer.spinner_stop()
|
|
342
|
-
display.renderer.stop_bottom_live()
|
|
343
|
-
elif (
|
|
344
|
-
isinstance(display, ui.DebugEventDisplay)
|
|
345
|
-
and display.wrapped_display
|
|
346
|
-
and isinstance(display.wrapped_display, ui.REPLDisplay)
|
|
347
|
-
):
|
|
348
|
-
display.wrapped_display.renderer.spinner_stop()
|
|
349
|
-
display.wrapped_display.renderer.stop_bottom_live()
|
|
350
|
-
|
|
351
|
-
# Pass the pre-detected theme to avoid redundant TTY queries.
|
|
352
|
-
# Querying the terminal background again after an interactive selection
|
|
353
|
-
# can interfere with prompt_toolkit's terminal state and break history navigation.
|
|
354
|
-
is_light_background: bool | None = None
|
|
355
|
-
if components.theme == "light":
|
|
356
|
-
is_light_background = True
|
|
357
|
-
elif components.theme == "dark":
|
|
358
|
-
is_light_background = False
|
|
359
|
-
|
|
360
|
-
def _get_active_session_id() -> str | None:
|
|
361
|
-
"""Get the current active session ID dynamically.
|
|
362
|
-
|
|
363
|
-
This is necessary because /clear command creates a new session with a different ID.
|
|
364
|
-
"""
|
|
365
|
-
|
|
366
|
-
return components.executor.context.current_session_id()
|
|
367
|
-
|
|
368
|
-
async def _change_model_from_prompt(model_name: str) -> None:
|
|
369
|
-
sid = _get_active_session_id()
|
|
370
|
-
if not sid:
|
|
371
|
-
return
|
|
372
|
-
await components.executor.submit_and_wait(
|
|
373
|
-
op.ChangeModelOperation(
|
|
374
|
-
session_id=sid,
|
|
375
|
-
model_name=model_name,
|
|
376
|
-
save_as_default=False,
|
|
377
|
-
defer_thinking_selection=True,
|
|
378
|
-
emit_welcome_event=True,
|
|
379
|
-
emit_switch_message=False,
|
|
380
|
-
)
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
def _get_current_llm_config() -> llm_param.LLMConfigParameter | None:
|
|
384
|
-
agent = components.executor.context.current_agent
|
|
385
|
-
if agent is None:
|
|
386
|
-
return None
|
|
387
|
-
return agent.profile.llm_client.get_llm_config()
|
|
388
|
-
|
|
389
|
-
async def _change_thinking_from_prompt(thinking: llm_param.Thinking) -> None:
|
|
390
|
-
sid = _get_active_session_id()
|
|
391
|
-
if not sid:
|
|
392
|
-
return
|
|
393
|
-
await components.executor.submit_and_wait(
|
|
394
|
-
op.ChangeThinkingOperation(
|
|
395
|
-
session_id=sid,
|
|
396
|
-
thinking=thinking,
|
|
397
|
-
emit_welcome_event=True,
|
|
398
|
-
emit_switch_message=False,
|
|
399
|
-
)
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
# Inject command name checker into user_input renderer (for slash command highlighting)
|
|
403
|
-
from klaude_code.ui.renderers.user_input import set_command_name_checker
|
|
404
|
-
|
|
405
|
-
set_command_name_checker(is_slash_command_name)
|
|
406
|
-
|
|
407
|
-
input_provider: ui.InputProviderABC = ui.PromptToolkitInput(
|
|
408
|
-
status_provider=_status_provider,
|
|
409
|
-
pre_prompt=_stop_rich_bottom_ui,
|
|
410
|
-
is_light_background=is_light_background,
|
|
411
|
-
get_current_model_config_name=lambda: (
|
|
412
|
-
components.executor.context.current_agent.session.model_config_name
|
|
413
|
-
if components.executor.context.current_agent is not None
|
|
414
|
-
else None
|
|
415
|
-
),
|
|
416
|
-
on_change_model=_change_model_from_prompt,
|
|
417
|
-
get_current_llm_config=_get_current_llm_config,
|
|
418
|
-
on_change_thinking=_change_thinking_from_prompt,
|
|
419
|
-
command_info_provider=get_command_info_list,
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
# --- Custom Ctrl+C handler: double-press within 2s to exit, single press shows toast ---
|
|
423
|
-
def _show_toast_once() -> None:
|
|
424
|
-
MSG = "Press ctrl+c again to exit"
|
|
425
|
-
try:
|
|
426
|
-
# Keep message short; avoid interfering with spinner layout
|
|
427
|
-
printer: PrintCapable | None = None
|
|
428
|
-
|
|
429
|
-
# Check if it's a REPLDisplay with renderer
|
|
430
|
-
if isinstance(components.display, ui.REPLDisplay):
|
|
431
|
-
printer = components.display.renderer
|
|
432
|
-
# Check if it's a DebugEventDisplay wrapping a REPLDisplay
|
|
433
|
-
elif (
|
|
434
|
-
isinstance(components.display, ui.DebugEventDisplay)
|
|
435
|
-
and components.display.wrapped_display
|
|
436
|
-
and isinstance(components.display.wrapped_display, ui.REPLDisplay)
|
|
437
|
-
):
|
|
438
|
-
printer = components.display.wrapped_display.renderer
|
|
439
|
-
|
|
440
|
-
if printer is not None:
|
|
441
|
-
printer.print(Text(f" {MSG} ", style="bold yellow reverse"))
|
|
442
|
-
else:
|
|
443
|
-
print(MSG, file=sys.stderr)
|
|
444
|
-
except (AttributeError, TypeError, RuntimeError):
|
|
445
|
-
# Fallback if themed print is unavailable (e.g., display not ready or Rich internal error)
|
|
446
|
-
print(MSG, file=sys.stderr)
|
|
447
|
-
|
|
448
|
-
def _hide_progress() -> None:
|
|
449
|
-
return
|
|
450
|
-
|
|
451
|
-
restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
|
|
452
|
-
|
|
453
|
-
exit_hint_printed = False
|
|
454
|
-
|
|
455
|
-
try:
|
|
456
|
-
await initialize_session(components.executor, components.event_queue, session_id=session_id)
|
|
457
|
-
_backfill_session_model_config(
|
|
458
|
-
components.executor.context.current_agent,
|
|
459
|
-
init_config.model,
|
|
460
|
-
components.config.main_model,
|
|
461
|
-
is_new_session=session_id is None,
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
# Input
|
|
465
|
-
await input_provider.start()
|
|
466
|
-
async for user_input in input_provider.iter_inputs():
|
|
467
|
-
# Handle special commands
|
|
468
|
-
if user_input.text.strip().lower() in {"exit", ":q", "quit"}:
|
|
469
|
-
break
|
|
470
|
-
elif user_input.text.strip() == "":
|
|
471
|
-
continue
|
|
472
|
-
# Use dynamic session_id lookup to handle /clear creating new sessions.
|
|
473
|
-
# UI/CLI parses commands and submits concrete operations; core executes operations.
|
|
474
|
-
active_session_id = _get_active_session_id()
|
|
475
|
-
is_interactive = has_interactive_command(user_input.text)
|
|
476
|
-
|
|
477
|
-
wait_id = await submit_user_input_payload(
|
|
478
|
-
executor=components.executor,
|
|
479
|
-
event_queue=components.event_queue,
|
|
480
|
-
user_input=user_input,
|
|
481
|
-
session_id=active_session_id,
|
|
482
|
-
)
|
|
483
|
-
|
|
484
|
-
if wait_id is None:
|
|
485
|
-
continue
|
|
486
|
-
|
|
487
|
-
if is_interactive:
|
|
488
|
-
await components.executor.wait_for(wait_id)
|
|
489
|
-
continue
|
|
490
|
-
|
|
491
|
-
# Esc monitor for long-running, interruptible operations
|
|
492
|
-
async def _on_esc_interrupt() -> None:
|
|
493
|
-
await components.executor.submit(op.InterruptOperation(target_session_id=_get_active_session_id()))
|
|
494
|
-
|
|
495
|
-
stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
|
|
496
|
-
# Wait for this specific task to complete before accepting next input
|
|
497
|
-
try:
|
|
498
|
-
await components.executor.wait_for(wait_id)
|
|
499
|
-
finally:
|
|
500
|
-
# Stop ESC monitor and wait for it to finish cleaning up TTY
|
|
501
|
-
stop_event.set()
|
|
502
|
-
with contextlib.suppress(Exception):
|
|
503
|
-
await esc_task
|
|
504
|
-
|
|
505
|
-
except KeyboardInterrupt:
|
|
506
|
-
await _handle_keyboard_interrupt(components.executor)
|
|
507
|
-
exit_hint_printed = True
|
|
508
|
-
finally:
|
|
509
|
-
# Restore original SIGINT handler
|
|
510
|
-
with contextlib.suppress(Exception):
|
|
511
|
-
restore_sigint()
|
|
512
|
-
await cleanup_app_components(components)
|
|
513
|
-
|
|
514
|
-
if not exit_hint_printed:
|
|
515
|
-
active_session_id = components.executor.context.current_session_id()
|
|
516
|
-
if active_session_id and Session.exists(active_session_id):
|
|
517
|
-
log(f"Session ID: {active_session_id}")
|
|
518
|
-
log(f"Resume with: klaude --resume-by-id {active_session_id}")
|
klaude_code/core/prompt.py
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import shutil
|
|
3
|
-
from functools import cache
|
|
4
|
-
from importlib.resources import files
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from klaude_code.protocol import llm_param
|
|
8
|
-
from klaude_code.protocol.sub_agent import get_sub_agent_profile
|
|
9
|
-
|
|
10
|
-
COMMAND_DESCRIPTIONS: dict[str, str] = {
|
|
11
|
-
"rg": "ripgrep - fast text search",
|
|
12
|
-
"fd": "simple and fast alternative to find",
|
|
13
|
-
"tree": "directory listing as a tree",
|
|
14
|
-
"sg": "ast-grep - AST-aware code search",
|
|
15
|
-
"jj": "jujutsu - Git-compatible version control system",
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
# Mapping from logical prompt keys to resource file paths under the core/prompt directory.
|
|
19
|
-
PROMPT_FILES: dict[str, str] = {
|
|
20
|
-
"main_codex": "prompts/prompt-codex.md",
|
|
21
|
-
"main_gpt_5_1_codex_max": "prompts/prompt-codex-gpt-5-1-codex-max.md",
|
|
22
|
-
"main_gpt_5_2_codex": "prompts/prompt-codex-gpt-5-2-codex.md",
|
|
23
|
-
"main": "prompts/prompt-claude-code.md",
|
|
24
|
-
"main_gemini": "prompts/prompt-gemini.md", # https://ai.google.dev/gemini-api/docs/prompting-strategies?hl=zh-cn#agentic-si-template
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@cache
|
|
29
|
-
def _load_prompt_by_path(prompt_path: str) -> str:
|
|
30
|
-
"""Load and cache prompt content from a file path relative to core package."""
|
|
31
|
-
return files(__package__).joinpath(prompt_path).read_text(encoding="utf-8").strip()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _load_base_prompt(file_key: str) -> str:
|
|
35
|
-
"""Load and cache the base prompt content from file."""
|
|
36
|
-
try:
|
|
37
|
-
prompt_path = PROMPT_FILES[file_key]
|
|
38
|
-
except KeyError as exc:
|
|
39
|
-
raise ValueError(f"Unknown prompt key: {file_key}") from exc
|
|
40
|
-
|
|
41
|
-
return _load_prompt_by_path(prompt_path)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _get_file_key(model_name: str, protocol: llm_param.LLMClientProtocol) -> str:
|
|
45
|
-
"""Determine which prompt file to use based on model."""
|
|
46
|
-
match model_name:
|
|
47
|
-
case name if "gpt-5.2-codex" in name:
|
|
48
|
-
return "main_gpt_5_2_codex"
|
|
49
|
-
case name if "gpt-5.1-codex-max" in name:
|
|
50
|
-
return "main_gpt_5_1_codex_max"
|
|
51
|
-
case name if "gpt-5" in name:
|
|
52
|
-
return "main_codex"
|
|
53
|
-
case name if "gemini" in name:
|
|
54
|
-
return "main_gemini"
|
|
55
|
-
case _:
|
|
56
|
-
return "main"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _build_env_info(model_name: str) -> str:
|
|
60
|
-
"""Build environment info section with dynamic runtime values."""
|
|
61
|
-
cwd = Path.cwd()
|
|
62
|
-
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
|
63
|
-
is_git_repo = (cwd / ".git").exists()
|
|
64
|
-
is_empty_dir = not any(cwd.iterdir())
|
|
65
|
-
|
|
66
|
-
available_tools: list[str] = []
|
|
67
|
-
for command, desc in COMMAND_DESCRIPTIONS.items():
|
|
68
|
-
if shutil.which(command) is not None:
|
|
69
|
-
available_tools.append(f"{command}: {desc}")
|
|
70
|
-
|
|
71
|
-
cwd_display = f"{cwd} (empty)" if is_empty_dir else str(cwd)
|
|
72
|
-
env_lines: list[str] = [
|
|
73
|
-
"",
|
|
74
|
-
"",
|
|
75
|
-
"Here is useful information about the environment you are running in:",
|
|
76
|
-
"<env>",
|
|
77
|
-
f"Working directory: {cwd_display}",
|
|
78
|
-
f"Today's Date: {today}",
|
|
79
|
-
f"Is directory a git repo: {is_git_repo}",
|
|
80
|
-
f"You are powered by the model: {model_name}",
|
|
81
|
-
]
|
|
82
|
-
|
|
83
|
-
if available_tools:
|
|
84
|
-
env_lines.append("Prefer to use the following CLI utilities:")
|
|
85
|
-
for tool in available_tools:
|
|
86
|
-
env_lines.append(f"- {tool}")
|
|
87
|
-
|
|
88
|
-
env_lines.append("</env>")
|
|
89
|
-
|
|
90
|
-
return "\n".join(env_lines)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def load_system_prompt(
|
|
94
|
-
model_name: str, protocol: llm_param.LLMClientProtocol, sub_agent_type: str | None = None
|
|
95
|
-
) -> str:
|
|
96
|
-
"""Get system prompt content for the given model and sub-agent type."""
|
|
97
|
-
if sub_agent_type is not None:
|
|
98
|
-
profile = get_sub_agent_profile(sub_agent_type)
|
|
99
|
-
base_prompt = _load_prompt_by_path(profile.prompt_file)
|
|
100
|
-
else:
|
|
101
|
-
file_key = _get_file_key(model_name, protocol)
|
|
102
|
-
base_prompt = _load_base_prompt(file_key)
|
|
103
|
-
|
|
104
|
-
if protocol == llm_param.LLMClientProtocol.CODEX_OAUTH:
|
|
105
|
-
# Do not append environment info for Codex protocol
|
|
106
|
-
return base_prompt
|
|
107
|
-
|
|
108
|
-
return base_prompt + _build_env_info(model_name)
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
Execute a skill within the main conversation
|
|
2
|
-
|
|
3
|
-
<skills_instructions>
|
|
4
|
-
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
|
|
5
|
-
|
|
6
|
-
How to use skills:
|
|
7
|
-
- Invoke skills using this tool with the skill name only (no arguments)
|
|
8
|
-
- When you invoke a skill, you will see <command-message>The "{name}" skill is loading</command-message>
|
|
9
|
-
- The skill's prompt will expand and provide detailed instructions on how to complete the task
|
|
10
|
-
|
|
11
|
-
Examples:
|
|
12
|
-
- command: "pdf" - invoke the pdf skill
|
|
13
|
-
- command: "xlsx" - invoke the xlsx skill
|
|
14
|
-
- command: "document-skills:pdf" - invoke using fully qualified name
|
|
15
|
-
|
|
16
|
-
Important:
|
|
17
|
-
- Only use skills listed in <available_skills> below
|
|
18
|
-
- Do not invoke a skill that is already running
|
|
19
|
-
- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)
|
|
20
|
-
</skills_instructions>
|
|
21
|
-
|
|
22
|
-
<available_skills>
|
|
23
|
-
$skills_xml
|
|
24
|
-
</available_skills>
|