klaude-code 1.2.15__py3-none-any.whl → 1.2.17__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/main.py +66 -42
- klaude_code/cli/runtime.py +34 -13
- klaude_code/command/__init__.py +3 -0
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/export_online_cmd.py +149 -0
- klaude_code/command/prompt-handoff.md +33 -0
- klaude_code/command/thinking_cmd.py +5 -1
- klaude_code/config/config.py +20 -21
- klaude_code/config/list_model.py +1 -1
- klaude_code/const/__init__.py +3 -0
- klaude_code/core/executor.py +2 -2
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +30 -6
- klaude_code/core/prompt.py +15 -13
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +0 -1
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +48 -0
- klaude_code/core/reminders.py +75 -32
- klaude_code/core/task.py +18 -22
- klaude_code/core/tool/__init__.py +4 -0
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/sub_agent_tool.py +6 -0
- klaude_code/core/tool/tool_runner.py +9 -1
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +126 -0
- klaude_code/core/turn.py +45 -4
- klaude_code/llm/anthropic/input.py +14 -5
- klaude_code/llm/openrouter/input.py +14 -3
- klaude_code/llm/responses/input.py +19 -0
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +9 -0
- klaude_code/protocol/model.py +24 -14
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +78 -0
- klaude_code/protocol/tools.py +2 -0
- klaude_code/session/export.py +12 -6
- klaude_code/session/session.py +12 -2
- klaude_code/session/templates/export_session.html +111 -36
- klaude_code/ui/modes/repl/completers.py +1 -1
- klaude_code/ui/modes/repl/event_handler.py +65 -8
- klaude_code/ui/modes/repl/renderer.py +11 -9
- klaude_code/ui/renderers/developer.py +18 -7
- klaude_code/ui/renderers/metadata.py +24 -12
- klaude_code/ui/renderers/sub_agent.py +63 -3
- klaude_code/ui/renderers/thinking.py +1 -1
- klaude_code/ui/renderers/tools.py +24 -37
- klaude_code/ui/rich/markdown.py +20 -48
- klaude_code/ui/rich/status.py +61 -17
- klaude_code/ui/rich/theme.py +8 -7
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.17.dist-info}/METADATA +114 -22
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.17.dist-info}/RECORD +57 -48
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/protocol/sub_agent.py +0 -354
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.17.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.17.dist-info}/entry_points.txt +0 -0
klaude_code/cli/main.py
CHANGED
|
@@ -13,7 +13,7 @@ from klaude_code.cli.debug import DEBUG_FILTER_HELP, open_log_file_in_editor, re
|
|
|
13
13
|
from klaude_code.cli.session_cmd import register_session_commands
|
|
14
14
|
from klaude_code.config import load_config
|
|
15
15
|
from klaude_code.session import Session, resume_select_session
|
|
16
|
-
from klaude_code.trace import prepare_debug_log_file
|
|
16
|
+
from klaude_code.trace import DebugType, prepare_debug_log_file
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def set_terminal_title(title: str) -> None:
|
|
@@ -22,6 +22,63 @@ def set_terminal_title(title: str) -> None:
|
|
|
22
22
|
sys.stdout.flush()
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def setup_terminal_title() -> None:
|
|
26
|
+
"""Set terminal title to current folder name."""
|
|
27
|
+
folder_name = os.path.basename(os.getcwd())
|
|
28
|
+
set_terminal_title(f"{folder_name}: klaude")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def prepare_debug_logging(debug: bool, debug_filter: str | None) -> tuple[bool, set[DebugType] | None, Path | None]:
|
|
32
|
+
"""Resolve debug settings and prepare log file if debugging is enabled.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A tuple of (debug_enabled, debug_filters, log_path).
|
|
36
|
+
log_path is None if debugging is disabled.
|
|
37
|
+
"""
|
|
38
|
+
debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
|
|
39
|
+
log_path: Path | None = None
|
|
40
|
+
if debug_enabled:
|
|
41
|
+
log_path = prepare_debug_log_file()
|
|
42
|
+
return debug_enabled, debug_filters, log_path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_input_content(cli_argument: str) -> str | None:
|
|
46
|
+
"""Read and merge input from stdin and CLI argument.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
cli_argument: The input content passed as CLI argument.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The merged input content, or None if no input was provided.
|
|
53
|
+
"""
|
|
54
|
+
from klaude_code.trace import log
|
|
55
|
+
|
|
56
|
+
parts: list[str] = []
|
|
57
|
+
|
|
58
|
+
# Handle stdin input
|
|
59
|
+
if not sys.stdin.isatty():
|
|
60
|
+
try:
|
|
61
|
+
stdin = sys.stdin.read().rstrip("\n")
|
|
62
|
+
if stdin:
|
|
63
|
+
parts.append(stdin)
|
|
64
|
+
except (OSError, ValueError) as e:
|
|
65
|
+
# Expected I/O-related errors when reading from stdin (e.g. broken pipe, closed stream).
|
|
66
|
+
log((f"Error reading from stdin: {e}", "red"))
|
|
67
|
+
except Exception as e:
|
|
68
|
+
# Unexpected errors are still reported but kept from crashing the CLI.
|
|
69
|
+
log((f"Unexpected error reading from stdin: {e}", "red"))
|
|
70
|
+
|
|
71
|
+
if cli_argument:
|
|
72
|
+
parts.append(cli_argument)
|
|
73
|
+
|
|
74
|
+
content = "\n".join(parts)
|
|
75
|
+
if len(content) == 0:
|
|
76
|
+
log(("Error: No input content provided", "red"))
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
return content
|
|
80
|
+
|
|
81
|
+
|
|
25
82
|
def _version_callback(value: bool) -> None:
|
|
26
83
|
"""Show version and exit."""
|
|
27
84
|
if value:
|
|
@@ -90,33 +147,10 @@ def exec_command(
|
|
|
90
147
|
),
|
|
91
148
|
) -> None:
|
|
92
149
|
"""Execute non-interactively with provided input."""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# Set terminal title with current folder name
|
|
96
|
-
folder_name = os.path.basename(os.getcwd())
|
|
97
|
-
set_terminal_title(f"{folder_name}: klaude")
|
|
98
|
-
|
|
99
|
-
parts: list[str] = []
|
|
150
|
+
setup_terminal_title()
|
|
100
151
|
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
-
try:
|
|
104
|
-
stdin = sys.stdin.read().rstrip("\n")
|
|
105
|
-
if stdin:
|
|
106
|
-
parts.append(stdin)
|
|
107
|
-
except (OSError, ValueError) as e:
|
|
108
|
-
# Expected I/O-related errors when reading from stdin (e.g. broken pipe, closed stream).
|
|
109
|
-
log((f"Error reading from stdin: {e}", "red"))
|
|
110
|
-
except Exception as e:
|
|
111
|
-
# Unexpected errors are still reported but kept from crashing the CLI.
|
|
112
|
-
log((f"Unexpected error reading from stdin: {e}", "red"))
|
|
113
|
-
|
|
114
|
-
if input_content:
|
|
115
|
-
parts.append(input_content)
|
|
116
|
-
|
|
117
|
-
input_content = "\n".join(parts)
|
|
118
|
-
if len(input_content) == 0:
|
|
119
|
-
log(("Error: No input content provided", "red"))
|
|
152
|
+
merged_input = read_input_content(input_content)
|
|
153
|
+
if merged_input is None:
|
|
120
154
|
raise typer.Exit(1)
|
|
121
155
|
|
|
122
156
|
from klaude_code.cli.runtime import AppInitConfig, run_exec
|
|
@@ -133,11 +167,7 @@ def exec_command(
|
|
|
133
167
|
if chosen_model is None:
|
|
134
168
|
return
|
|
135
169
|
|
|
136
|
-
debug_enabled, debug_filters =
|
|
137
|
-
|
|
138
|
-
log_path: Path | None = None
|
|
139
|
-
if debug_enabled:
|
|
140
|
-
log_path = prepare_debug_log_file()
|
|
170
|
+
debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
|
|
141
171
|
|
|
142
172
|
init_config = AppInitConfig(
|
|
143
173
|
model=chosen_model,
|
|
@@ -154,7 +184,7 @@ def exec_command(
|
|
|
154
184
|
asyncio.run(
|
|
155
185
|
run_exec(
|
|
156
186
|
init_config=init_config,
|
|
157
|
-
input_content=
|
|
187
|
+
input_content=merged_input,
|
|
158
188
|
)
|
|
159
189
|
)
|
|
160
190
|
|
|
@@ -210,10 +240,8 @@ def main_callback(
|
|
|
210
240
|
from klaude_code.cli.runtime import AppInitConfig, run_interactive
|
|
211
241
|
from klaude_code.config.select_model import select_model_from_config
|
|
212
242
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
set_terminal_title(f"{folder_name}: klaude")
|
|
216
|
-
# Interactive mode
|
|
243
|
+
setup_terminal_title()
|
|
244
|
+
|
|
217
245
|
chosen_model = model
|
|
218
246
|
if select_model:
|
|
219
247
|
chosen_model = select_model_from_config(preferred=model)
|
|
@@ -232,11 +260,7 @@ def main_callback(
|
|
|
232
260
|
session_id = Session.most_recent_session_id()
|
|
233
261
|
# If still no session_id, leave as None to create a new session
|
|
234
262
|
|
|
235
|
-
debug_enabled, debug_filters =
|
|
236
|
-
|
|
237
|
-
log_path: Path | None = None
|
|
238
|
-
if debug_enabled:
|
|
239
|
-
log_path = prepare_debug_log_file()
|
|
263
|
+
debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
|
|
240
264
|
|
|
241
265
|
init_config = AppInitConfig(
|
|
242
266
|
model=chosen_model,
|
klaude_code/cli/runtime.py
CHANGED
|
@@ -127,6 +127,28 @@ async def initialize_app_components(init_config: AppInitConfig) -> AppComponents
|
|
|
127
127
|
)
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
async def initialize_session(
|
|
131
|
+
executor: Executor,
|
|
132
|
+
event_queue: asyncio.Queue[events.Event],
|
|
133
|
+
session_id: str | None = None,
|
|
134
|
+
) -> str | None:
|
|
135
|
+
"""Initialize a session and return the active session ID.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
executor: The executor to submit operations to.
|
|
139
|
+
event_queue: The event queue for synchronization.
|
|
140
|
+
session_id: Optional session ID to resume. If None, creates a new session.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The active session ID, or None if no session is active.
|
|
144
|
+
"""
|
|
145
|
+
await executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
|
|
146
|
+
await event_queue.join()
|
|
147
|
+
|
|
148
|
+
active_session_ids = executor.context.agent_manager.active_session_ids()
|
|
149
|
+
return active_session_ids[0] if active_session_ids else session_id
|
|
150
|
+
|
|
151
|
+
|
|
130
152
|
async def cleanup_app_components(components: AppComponents) -> None:
|
|
131
153
|
"""Clean up all application components."""
|
|
132
154
|
try:
|
|
@@ -167,13 +189,7 @@ async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
|
|
|
167
189
|
components = await initialize_app_components(init_config)
|
|
168
190
|
|
|
169
191
|
try:
|
|
170
|
-
|
|
171
|
-
await components.executor.submit_and_wait(op.InitAgentOperation())
|
|
172
|
-
await components.event_queue.join()
|
|
173
|
-
|
|
174
|
-
# Get the session_id from the newly created agent
|
|
175
|
-
session_ids = components.executor.context.agent_manager.active_session_ids()
|
|
176
|
-
session_id = session_ids[0] if session_ids else None
|
|
192
|
+
session_id = await initialize_session(components.executor, components.event_queue)
|
|
177
193
|
|
|
178
194
|
# Submit the input content directly
|
|
179
195
|
await components.executor.submit_and_wait(
|
|
@@ -245,12 +261,15 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
245
261
|
restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
|
|
246
262
|
|
|
247
263
|
try:
|
|
248
|
-
await components.executor.
|
|
249
|
-
|
|
264
|
+
await initialize_session(components.executor, components.event_queue, session_id=session_id)
|
|
265
|
+
|
|
266
|
+
def _get_active_session_id() -> str | None:
|
|
267
|
+
"""Get the current active session ID dynamically.
|
|
250
268
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
This is necessary because /clear command creates a new session with a different ID.
|
|
270
|
+
"""
|
|
271
|
+
active_ids = components.executor.context.agent_manager.active_session_ids()
|
|
272
|
+
return active_ids[0] if active_ids else None
|
|
254
273
|
|
|
255
274
|
# Input
|
|
256
275
|
await input_provider.start()
|
|
@@ -261,6 +280,8 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
261
280
|
elif user_input.text.strip() == "":
|
|
262
281
|
continue
|
|
263
282
|
# Submit user input operation - directly use the payload from iter_inputs
|
|
283
|
+
# Use dynamic session_id lookup to handle /clear creating new sessions
|
|
284
|
+
active_session_id = _get_active_session_id()
|
|
264
285
|
submission_id = await components.executor.submit(
|
|
265
286
|
op.UserInputOperation(input=user_input, session_id=active_session_id)
|
|
266
287
|
)
|
|
@@ -271,7 +292,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
271
292
|
else:
|
|
272
293
|
# Esc monitor for long-running, interruptible operations
|
|
273
294
|
async def _on_esc_interrupt() -> None:
|
|
274
|
-
await components.executor.submit(op.InterruptOperation(target_session_id=
|
|
295
|
+
await components.executor.submit(op.InterruptOperation(target_session_id=_get_active_session_id()))
|
|
275
296
|
|
|
276
297
|
stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
|
|
277
298
|
# Wait for this specific task to complete before accepting next input
|
klaude_code/command/__init__.py
CHANGED
|
@@ -30,6 +30,7 @@ def ensure_commands_loaded() -> None:
|
|
|
30
30
|
from .clear_cmd import ClearCommand
|
|
31
31
|
from .diff_cmd import DiffCommand
|
|
32
32
|
from .export_cmd import ExportCommand
|
|
33
|
+
from .export_online_cmd import ExportOnlineCommand
|
|
33
34
|
from .help_cmd import HelpCommand
|
|
34
35
|
from .model_cmd import ModelCommand
|
|
35
36
|
from .refresh_cmd import RefreshTerminalCommand
|
|
@@ -40,6 +41,7 @@ def ensure_commands_loaded() -> None:
|
|
|
40
41
|
|
|
41
42
|
# Register in desired display order
|
|
42
43
|
register(ExportCommand())
|
|
44
|
+
register(ExportOnlineCommand())
|
|
43
45
|
register(RefreshTerminalCommand())
|
|
44
46
|
register(ThinkingCommand())
|
|
45
47
|
register(ModelCommand())
|
|
@@ -60,6 +62,7 @@ def __getattr__(name: str) -> object:
|
|
|
60
62
|
"ClearCommand": "clear_cmd",
|
|
61
63
|
"DiffCommand": "diff_cmd",
|
|
62
64
|
"ExportCommand": "export_cmd",
|
|
65
|
+
"ExportOnlineCommand": "export_online_cmd",
|
|
63
66
|
"HelpCommand": "help_cmd",
|
|
64
67
|
"ModelCommand": "model_cmd",
|
|
65
68
|
"RefreshTerminalCommand": "refresh_cmd",
|
|
@@ -25,7 +25,7 @@ class ExportCommand(CommandABC):
|
|
|
25
25
|
|
|
26
26
|
@property
|
|
27
27
|
def support_addition_params(self) -> bool:
|
|
28
|
-
return
|
|
28
|
+
return False
|
|
29
29
|
|
|
30
30
|
@property
|
|
31
31
|
def is_interactive(self) -> bool:
|
|
@@ -33,7 +33,7 @@ class ExportCommand(CommandABC):
|
|
|
33
33
|
|
|
34
34
|
async def run(self, raw: str, agent: Agent) -> CommandResult:
|
|
35
35
|
try:
|
|
36
|
-
output_path = self._resolve_output_path(
|
|
36
|
+
output_path = self._resolve_output_path("", agent)
|
|
37
37
|
html_doc = self._build_html(agent)
|
|
38
38
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
39
|
output_path.write_text(html_doc, encoding="utf-8")
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from klaude_code.command.command_abc import CommandABC, CommandResult
|
|
11
|
+
from klaude_code.protocol import commands, events, model
|
|
12
|
+
from klaude_code.session.export import build_export_html
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from klaude_code.core.agent import Agent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExportOnlineCommand(CommandABC):
|
|
19
|
+
"""Export and deploy the current session to surge.sh as a static webpage."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> commands.CommandName:
|
|
23
|
+
return commands.CommandName.EXPORT_ONLINE
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def summary(self) -> str:
|
|
27
|
+
return "Export and deploy session to surge.sh"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def support_addition_params(self) -> bool:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def is_interactive(self) -> bool:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
async def run(self, raw: str, agent: Agent) -> CommandResult:
|
|
38
|
+
# Check if npx or surge is available
|
|
39
|
+
surge_cmd = self._get_surge_command()
|
|
40
|
+
if not surge_cmd:
|
|
41
|
+
event = events.DeveloperMessageEvent(
|
|
42
|
+
session_id=agent.session.id,
|
|
43
|
+
item=model.DeveloperMessageItem(
|
|
44
|
+
content="surge.sh CLI not found. Install with: npm install -g surge",
|
|
45
|
+
command_output=model.CommandOutput(command_name=self.name, is_error=True),
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
return CommandResult(events=[event])
|
|
49
|
+
|
|
50
|
+
# Check if user is logged in to surge
|
|
51
|
+
if not self._is_surge_logged_in(surge_cmd):
|
|
52
|
+
login_cmd = " ".join([*surge_cmd, "login"])
|
|
53
|
+
event = events.DeveloperMessageEvent(
|
|
54
|
+
session_id=agent.session.id,
|
|
55
|
+
item=model.DeveloperMessageItem(
|
|
56
|
+
content=f"Not logged in to surge.sh. Please run: {login_cmd}",
|
|
57
|
+
command_output=model.CommandOutput(command_name=self.name, is_error=True),
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
return CommandResult(events=[event])
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
html_doc = self._build_html(agent)
|
|
64
|
+
domain = self._generate_domain()
|
|
65
|
+
url = self._deploy_to_surge(surge_cmd, html_doc, domain)
|
|
66
|
+
|
|
67
|
+
event = events.DeveloperMessageEvent(
|
|
68
|
+
session_id=agent.session.id,
|
|
69
|
+
item=model.DeveloperMessageItem(
|
|
70
|
+
content=f"Session deployed to: {url}",
|
|
71
|
+
command_output=model.CommandOutput(command_name=self.name),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
return CommandResult(events=[event])
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
import traceback
|
|
77
|
+
|
|
78
|
+
event = events.DeveloperMessageEvent(
|
|
79
|
+
session_id=agent.session.id,
|
|
80
|
+
item=model.DeveloperMessageItem(
|
|
81
|
+
content=f"Failed to deploy session: {exc}\n{traceback.format_exc()}",
|
|
82
|
+
command_output=model.CommandOutput(command_name=self.name, is_error=True),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
return CommandResult(events=[event])
|
|
86
|
+
|
|
87
|
+
def _get_surge_command(self) -> list[str] | None:
|
|
88
|
+
"""Check if surge CLI is available, prefer npx if available."""
|
|
89
|
+
# Check for npx first (more common)
|
|
90
|
+
if shutil.which("npx"):
|
|
91
|
+
return ["npx", "surge"]
|
|
92
|
+
# Check for globally installed surge
|
|
93
|
+
if shutil.which("surge"):
|
|
94
|
+
return ["surge"]
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def _is_surge_logged_in(self, surge_cmd: list[str]) -> bool:
|
|
98
|
+
"""Check if user is logged in to surge.sh via 'surge whoami'."""
|
|
99
|
+
try:
|
|
100
|
+
cmd = [*surge_cmd, "whoami"]
|
|
101
|
+
result = subprocess.run(
|
|
102
|
+
cmd,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
timeout=30,
|
|
106
|
+
)
|
|
107
|
+
# If logged in, whoami returns 0 and prints the email
|
|
108
|
+
# If not logged in, it returns non-zero or prints "Not Authenticated"
|
|
109
|
+
if result.returncode != 0:
|
|
110
|
+
return False
|
|
111
|
+
output = (result.stdout + result.stderr).lower()
|
|
112
|
+
if "not authenticated" in output or "not logged in" in output:
|
|
113
|
+
return False
|
|
114
|
+
return bool(result.stdout.strip())
|
|
115
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
def _generate_domain(self) -> str:
|
|
119
|
+
"""Generate a random subdomain for surge.sh."""
|
|
120
|
+
random_suffix = secrets.token_hex(4)
|
|
121
|
+
return f"klaude-session-{random_suffix}.surge.sh"
|
|
122
|
+
|
|
123
|
+
def _deploy_to_surge(self, surge_cmd: list[str], html_content: str, domain: str) -> str:
|
|
124
|
+
"""Deploy HTML content to surge.sh and return the URL."""
|
|
125
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
126
|
+
html_path = Path(tmpdir) / "index.html"
|
|
127
|
+
html_path.write_text(html_content, encoding="utf-8")
|
|
128
|
+
|
|
129
|
+
# Run surge with --domain flag
|
|
130
|
+
cmd = [*surge_cmd, tmpdir, "--domain", domain]
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
cmd,
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
timeout=60,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if result.returncode != 0:
|
|
139
|
+
error_msg = result.stderr or result.stdout or "Unknown error"
|
|
140
|
+
raise RuntimeError(f"Surge deployment failed: {error_msg}")
|
|
141
|
+
|
|
142
|
+
return f"https://{domain}"
|
|
143
|
+
|
|
144
|
+
def _build_html(self, agent: Agent) -> str:
|
|
145
|
+
profile = agent.profile
|
|
146
|
+
system_prompt = (profile.system_prompt if profile else "") or ""
|
|
147
|
+
tools = profile.tools if profile else []
|
|
148
|
+
model_name = profile.llm_client.model_name if profile else "unknown"
|
|
149
|
+
return build_export_html(agent.session, system_prompt, tools, model_name)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Write a HANDOFF.md for another agent to continue the conversation
|
|
3
|
+
from: amp-cli https://ampcode.com/manual#handoff
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Write a HANDOFF.md file in the current working directory for another agent to continue this conversation.
|
|
7
|
+
|
|
8
|
+
Extract relevant context from the conversation above to facilitate continuing this work. Write from my perspective (first person: "I did...", "I told you...").
|
|
9
|
+
|
|
10
|
+
Consider what would be useful to know based on my request below. Relevant questions include:
|
|
11
|
+
- What did I just do or implement?
|
|
12
|
+
- What instructions did I give you that are still relevant (e.g., follow patterns in the codebase)?
|
|
13
|
+
- Did I provide a plan or spec that should be included?
|
|
14
|
+
- What important information did I share (certain libraries, patterns, constraints, preferences)?
|
|
15
|
+
- What key technical details did I discover (APIs, methods, patterns)?
|
|
16
|
+
- What caveats, limitations, or open questions remain?
|
|
17
|
+
|
|
18
|
+
Extract only what matters for the specific request below. Skip irrelevant questions. Choose an appropriate length based on the complexity of the request.
|
|
19
|
+
|
|
20
|
+
Focus on capabilities and behavior, not file-by-file changes. Avoid excessive implementation details (variable names, storage keys, constants) unless critical.
|
|
21
|
+
|
|
22
|
+
Format: Plain text with bullets. No markdown headers, no bold/italic, no code fences. Use workspace-relative paths for files.
|
|
23
|
+
|
|
24
|
+
List file or directory paths (workspace-relative) relevant to accomplishing the goal in the following format:
|
|
25
|
+
<example>
|
|
26
|
+
@src/project/main.py
|
|
27
|
+
@src/project/llm/
|
|
28
|
+
</example>
|
|
29
|
+
|
|
30
|
+
My request:
|
|
31
|
+
$ARGUMENTS
|
|
32
|
+
|
|
33
|
+
<system>If the request section is empty, ask for clarification about the goal</system>
|
|
@@ -211,7 +211,11 @@ class ThinkingCommand(CommandABC):
|
|
|
211
211
|
content=f"Thinking changed: {current} -> {new_status}",
|
|
212
212
|
command_output=model.CommandOutput(command_name=self.name),
|
|
213
213
|
),
|
|
214
|
-
)
|
|
214
|
+
),
|
|
215
|
+
events.WelcomeEvent(
|
|
216
|
+
work_dir=str(agent.session.work_dir),
|
|
217
|
+
llm_config=config,
|
|
218
|
+
),
|
|
215
219
|
]
|
|
216
220
|
)
|
|
217
221
|
|
klaude_code/config/config.py
CHANGED
|
@@ -23,20 +23,20 @@ class Config(BaseModel):
|
|
|
23
23
|
provider_list: list[llm_param.LLMConfigProviderParameter]
|
|
24
24
|
model_list: list[ModelConfig]
|
|
25
25
|
main_model: str
|
|
26
|
-
|
|
26
|
+
sub_agent_models: dict[str, str] = Field(default_factory=dict)
|
|
27
27
|
theme: str | None = None
|
|
28
28
|
|
|
29
29
|
@model_validator(mode="before")
|
|
30
30
|
@classmethod
|
|
31
|
-
def
|
|
32
|
-
raw_val: Any = data.get("
|
|
31
|
+
def _normalize_sub_agent_models(cls, data: dict[str, Any]) -> dict[str, Any]:
|
|
32
|
+
raw_val: Any = data.get("sub_agent_models") or {}
|
|
33
33
|
raw_models: dict[str, Any] = cast(dict[str, Any], raw_val) if isinstance(raw_val, dict) else {}
|
|
34
34
|
normalized: dict[str, str] = {}
|
|
35
35
|
key_map = {p.name.lower(): p.name for p in iter_sub_agent_profiles()}
|
|
36
36
|
for key, value in dict(raw_models).items():
|
|
37
37
|
canonical = key_map.get(str(key).lower(), str(key))
|
|
38
38
|
normalized[canonical] = str(value)
|
|
39
|
-
data["
|
|
39
|
+
data["sub_agent_models"] = normalized
|
|
40
40
|
return data
|
|
41
41
|
|
|
42
42
|
def get_main_model_config(self) -> llm_param.LLMConfigParameter:
|
|
@@ -79,8 +79,8 @@ class Config(BaseModel):
|
|
|
79
79
|
|
|
80
80
|
def get_example_config() -> Config:
|
|
81
81
|
return Config(
|
|
82
|
-
main_model="
|
|
83
|
-
|
|
82
|
+
main_model="opus",
|
|
83
|
+
sub_agent_models={"explore": "haiku", "oracle": "gpt-5.1", "webagent": "haiku", "task": "opus"},
|
|
84
84
|
provider_list=[
|
|
85
85
|
llm_param.LLMConfigProviderParameter(
|
|
86
86
|
provider_name="openai",
|
|
@@ -93,6 +93,11 @@ def get_example_config() -> Config:
|
|
|
93
93
|
protocol=llm_param.LLMClientProtocol.OPENROUTER,
|
|
94
94
|
api_key="your-openrouter-api-key",
|
|
95
95
|
),
|
|
96
|
+
llm_param.LLMConfigProviderParameter(
|
|
97
|
+
provider_name="anthropic",
|
|
98
|
+
protocol=llm_param.LLMClientProtocol.ANTHROPIC,
|
|
99
|
+
api_key="your-anthropic-api-key",
|
|
100
|
+
),
|
|
96
101
|
],
|
|
97
102
|
model_list=[
|
|
98
103
|
ModelConfig(
|
|
@@ -100,31 +105,25 @@ def get_example_config() -> Config:
|
|
|
100
105
|
provider="openai",
|
|
101
106
|
model_params=llm_param.LLMConfigModelParameter(
|
|
102
107
|
model="gpt-5.1-2025-11-13",
|
|
103
|
-
max_tokens=32000,
|
|
104
108
|
verbosity="medium",
|
|
105
109
|
thinking=llm_param.Thinking(
|
|
106
|
-
reasoning_effort="
|
|
110
|
+
reasoning_effort="high",
|
|
107
111
|
reasoning_summary="auto",
|
|
108
|
-
type="enabled",
|
|
109
|
-
budget_tokens=None,
|
|
110
112
|
),
|
|
111
|
-
context_limit=
|
|
113
|
+
context_limit=400000,
|
|
112
114
|
),
|
|
113
115
|
),
|
|
114
116
|
ModelConfig(
|
|
115
|
-
model_name="
|
|
116
|
-
provider="
|
|
117
|
+
model_name="opus",
|
|
118
|
+
provider="anthropic",
|
|
117
119
|
model_params=llm_param.LLMConfigModelParameter(
|
|
118
|
-
model="
|
|
119
|
-
|
|
120
|
-
verbosity="medium",
|
|
120
|
+
model="claude-opus-4-5-20251101",
|
|
121
|
+
verbosity="high",
|
|
121
122
|
thinking=llm_param.Thinking(
|
|
122
|
-
reasoning_effort="high",
|
|
123
|
-
reasoning_summary="auto",
|
|
124
123
|
type="enabled",
|
|
125
|
-
budget_tokens=
|
|
124
|
+
budget_tokens=31999,
|
|
126
125
|
),
|
|
127
|
-
context_limit=
|
|
126
|
+
context_limit=200000,
|
|
128
127
|
),
|
|
129
128
|
),
|
|
130
129
|
ModelConfig(
|
|
@@ -136,7 +135,7 @@ def get_example_config() -> Config:
|
|
|
136
135
|
provider_routing=llm_param.OpenRouterProviderRouting(
|
|
137
136
|
sort="throughput",
|
|
138
137
|
),
|
|
139
|
-
context_limit=
|
|
138
|
+
context_limit=200000,
|
|
140
139
|
),
|
|
141
140
|
),
|
|
142
141
|
],
|
klaude_code/config/list_model.py
CHANGED
|
@@ -198,7 +198,7 @@ def display_models_and_providers(config: Config):
|
|
|
198
198
|
)
|
|
199
199
|
|
|
200
200
|
for profile in iter_sub_agent_profiles():
|
|
201
|
-
sub_model_name = config.
|
|
201
|
+
sub_model_name = config.sub_agent_models.get(profile.name)
|
|
202
202
|
if not sub_model_name:
|
|
203
203
|
continue
|
|
204
204
|
console.print(
|
klaude_code/const/__init__.py
CHANGED
|
@@ -105,6 +105,9 @@ UI_REFRESH_RATE_FPS = 20
|
|
|
105
105
|
# Number of lines to keep visible at bottom of markdown streaming window
|
|
106
106
|
MARKDOWN_STREAM_LIVE_WINDOW = 20
|
|
107
107
|
|
|
108
|
+
# Status hint text shown after spinner status
|
|
109
|
+
STATUS_HINT_TEXT = " (esc to interrupt)"
|
|
110
|
+
|
|
108
111
|
# Status shimmer animation
|
|
109
112
|
# Horizontal padding used when computing shimmer band position
|
|
110
113
|
STATUS_SHIMMER_PADDING = 10
|
klaude_code/core/executor.py
CHANGED
|
@@ -96,7 +96,7 @@ class ExecutorContext:
|
|
|
96
96
|
# Delegate responsibilities to helper components
|
|
97
97
|
self.agent_manager = AgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
98
98
|
self.task_manager = TaskManager()
|
|
99
|
-
self.
|
|
99
|
+
self.sub_agent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
|
|
100
100
|
|
|
101
101
|
async def emit_event(self, event: events.Event) -> None:
|
|
102
102
|
"""Emit an event to the UI display system."""
|
|
@@ -240,7 +240,7 @@ class ExecutorContext:
|
|
|
240
240
|
|
|
241
241
|
# Inject subtask runner into tool context for nested Task tool usage
|
|
242
242
|
async def _runner(state: model.SubAgentState) -> SubAgentResult:
|
|
243
|
-
return await self.
|
|
243
|
+
return await self.sub_agent_manager.run_sub_agent(agent, state)
|
|
244
244
|
|
|
245
245
|
token = current_run_subtask_callback.set(_runner)
|
|
246
246
|
try:
|
|
@@ -32,7 +32,7 @@ def build_llm_clients(
|
|
|
32
32
|
sub_clients: dict[SubAgentType, LLMClientABC] = {}
|
|
33
33
|
|
|
34
34
|
for profile in iter_sub_agent_profiles():
|
|
35
|
-
model_name = config.
|
|
35
|
+
model_name = config.sub_agent_models.get(profile.name)
|
|
36
36
|
if not model_name:
|
|
37
37
|
continue
|
|
38
38
|
|