klaude-code 1.2.6__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- 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 +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import subprocess
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
|
|
6
|
-
from klaude_code.command.command_abc import CommandABC, CommandResult
|
|
7
|
-
from klaude_code.
|
|
8
|
-
from klaude_code.core.agent import Agent
|
|
9
|
-
from klaude_code.protocol import commands, events, model
|
|
10
|
-
from klaude_code.session.export import build_export_html, get_default_export_path
|
|
5
|
+
from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
|
|
6
|
+
from klaude_code.protocol import commands, model, op
|
|
11
7
|
|
|
12
8
|
|
|
13
|
-
@register_command
|
|
14
9
|
class ExportCommand(CommandABC):
|
|
15
10
|
"""Export the current session into a standalone HTML transcript."""
|
|
16
11
|
|
|
@@ -26,38 +21,26 @@ class ExportCommand(CommandABC):
|
|
|
26
21
|
def support_addition_params(self) -> bool:
|
|
27
22
|
return True
|
|
28
23
|
|
|
24
|
+
@property
|
|
25
|
+
def placeholder(self) -> str:
|
|
26
|
+
return "output path"
|
|
27
|
+
|
|
29
28
|
@property
|
|
30
29
|
def is_interactive(self) -> bool:
|
|
31
30
|
return False
|
|
32
31
|
|
|
33
|
-
async def run(self,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
),
|
|
46
|
-
)
|
|
47
|
-
return CommandResult(events=[event])
|
|
48
|
-
except Exception as exc: # pragma: no cover - safeguard for unexpected errors
|
|
49
|
-
import traceback
|
|
50
|
-
|
|
51
|
-
event = events.DeveloperMessageEvent(
|
|
52
|
-
session_id=agent.session.id,
|
|
53
|
-
item=model.DeveloperMessageItem(
|
|
54
|
-
content=f"Failed to export session: {exc}\n{traceback.format_exc()}",
|
|
55
|
-
command_output=model.CommandOutput(command_name=self.name, is_error=True),
|
|
56
|
-
),
|
|
57
|
-
)
|
|
58
|
-
return CommandResult(events=[event])
|
|
59
|
-
|
|
60
|
-
def _resolve_output_path(self, raw: str, agent: Agent) -> Path:
|
|
32
|
+
async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
|
|
33
|
+
output_path = self._normalize_output_path(user_input.text, agent)
|
|
34
|
+
return CommandResult(
|
|
35
|
+
operations=[
|
|
36
|
+
op.ExportSessionOperation(
|
|
37
|
+
session_id=agent.session.id,
|
|
38
|
+
output_path=str(output_path) if output_path is not None else None,
|
|
39
|
+
)
|
|
40
|
+
]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def _normalize_output_path(self, raw: str, agent: Agent) -> Path | None:
|
|
61
44
|
trimmed = raw.strip()
|
|
62
45
|
if trimmed:
|
|
63
46
|
candidate = Path(trimmed).expanduser()
|
|
@@ -66,21 +49,4 @@ class ExportCommand(CommandABC):
|
|
|
66
49
|
if candidate.suffix.lower() != ".html":
|
|
67
50
|
candidate = candidate.with_suffix(".html")
|
|
68
51
|
return candidate
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
def _open_file(self, path: Path) -> None:
|
|
72
|
-
try:
|
|
73
|
-
subprocess.run(["open", str(path)], check=True)
|
|
74
|
-
except FileNotFoundError as exc: # pragma: no cover - depends on platform
|
|
75
|
-
msg = "`open` command not found; please open the HTML manually."
|
|
76
|
-
raise RuntimeError(msg) from exc
|
|
77
|
-
except subprocess.CalledProcessError as exc: # pragma: no cover - depends on platform
|
|
78
|
-
msg = f"Failed to open HTML with `open`: {exc}"
|
|
79
|
-
raise RuntimeError(msg) from exc
|
|
80
|
-
|
|
81
|
-
def _build_html(self, agent: Agent) -> str:
|
|
82
|
-
profile = agent.profile
|
|
83
|
-
system_prompt = (profile.system_prompt if profile else "") or ""
|
|
84
|
-
tools = profile.tools if profile else []
|
|
85
|
-
model_name = profile.llm_client.model_name if profile else "unknown"
|
|
86
|
-
return build_export_html(agent.session, system_prompt, tools, model_name)
|
|
52
|
+
return None
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
|
|
13
|
+
from klaude_code.protocol import commands, events, model
|
|
14
|
+
from klaude_code.session.export import build_export_html
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExportOnlineCommand(CommandABC):
|
|
18
|
+
"""Export and deploy the current session to surge.sh as a static webpage."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self) -> commands.CommandName:
|
|
22
|
+
return commands.CommandName.EXPORT_ONLINE
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def summary(self) -> str:
|
|
26
|
+
return "Export and deploy session to surge.sh"
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def support_addition_params(self) -> bool:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_interactive(self) -> bool:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
|
|
37
|
+
del user_input # unused
|
|
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
|
+
try:
|
|
51
|
+
console = Console()
|
|
52
|
+
# Check login status inside status context since npx surge whoami can be slow
|
|
53
|
+
with console.status(Text("Checking surge.sh login status...", style="dim"), spinner_style="dim"):
|
|
54
|
+
logged_in = self._is_surge_logged_in(surge_cmd)
|
|
55
|
+
|
|
56
|
+
if not logged_in:
|
|
57
|
+
login_cmd = " ".join([*surge_cmd, "login"])
|
|
58
|
+
event = events.DeveloperMessageEvent(
|
|
59
|
+
session_id=agent.session.id,
|
|
60
|
+
item=model.DeveloperMessageItem(
|
|
61
|
+
content=f"Not logged in to surge.sh. Please run: {login_cmd}",
|
|
62
|
+
command_output=model.CommandOutput(command_name=self.name, is_error=True),
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
return CommandResult(events=[event])
|
|
66
|
+
|
|
67
|
+
with console.status(Text("Deploying to surge.sh...", style="dim"), spinner_style="dim"):
|
|
68
|
+
html_doc = self._build_html(agent)
|
|
69
|
+
domain = self._generate_domain()
|
|
70
|
+
url = self._deploy_to_surge(surge_cmd, html_doc, domain)
|
|
71
|
+
|
|
72
|
+
event = events.DeveloperMessageEvent(
|
|
73
|
+
session_id=agent.session.id,
|
|
74
|
+
item=model.DeveloperMessageItem(
|
|
75
|
+
content=f"Session deployed to: {url}",
|
|
76
|
+
command_output=model.CommandOutput(command_name=self.name),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
return CommandResult(events=[event])
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
import traceback
|
|
82
|
+
|
|
83
|
+
event = events.DeveloperMessageEvent(
|
|
84
|
+
session_id=agent.session.id,
|
|
85
|
+
item=model.DeveloperMessageItem(
|
|
86
|
+
content=f"Failed to deploy session: {exc}\n{traceback.format_exc()}",
|
|
87
|
+
command_output=model.CommandOutput(command_name=self.name, is_error=True),
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
return CommandResult(events=[event])
|
|
91
|
+
|
|
92
|
+
def _get_surge_command(self) -> list[str] | None:
|
|
93
|
+
"""Check if surge CLI is available, prefer npx if available."""
|
|
94
|
+
# Check for npx first (more common)
|
|
95
|
+
if shutil.which("npx"):
|
|
96
|
+
return ["npx", "surge"]
|
|
97
|
+
# Check for globally installed surge
|
|
98
|
+
if shutil.which("surge"):
|
|
99
|
+
return ["surge"]
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def _is_surge_logged_in(self, surge_cmd: list[str]) -> bool:
|
|
103
|
+
"""Check if user is logged in to surge.sh via 'surge whoami'."""
|
|
104
|
+
try:
|
|
105
|
+
cmd = [*surge_cmd, "whoami"]
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
cmd,
|
|
108
|
+
capture_output=True,
|
|
109
|
+
text=True,
|
|
110
|
+
timeout=30,
|
|
111
|
+
)
|
|
112
|
+
# If logged in, whoami returns 0 and prints the email
|
|
113
|
+
# If not logged in, it returns non-zero or prints "Not Authenticated"
|
|
114
|
+
if result.returncode != 0:
|
|
115
|
+
return False
|
|
116
|
+
output = (result.stdout + result.stderr).lower()
|
|
117
|
+
if "not authenticated" in output or "not logged in" in output:
|
|
118
|
+
return False
|
|
119
|
+
return bool(result.stdout.strip())
|
|
120
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def _generate_domain(self) -> str:
|
|
124
|
+
"""Generate a random subdomain for surge.sh."""
|
|
125
|
+
random_suffix = secrets.token_hex(4)
|
|
126
|
+
return f"klaude-session-{random_suffix}.surge.sh"
|
|
127
|
+
|
|
128
|
+
def _deploy_to_surge(self, surge_cmd: list[str], html_content: str, domain: str) -> str:
|
|
129
|
+
"""Deploy HTML content to surge.sh and return the URL."""
|
|
130
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
131
|
+
html_path = Path(tmpdir) / "index.html"
|
|
132
|
+
html_path.write_text(html_content, encoding="utf-8")
|
|
133
|
+
|
|
134
|
+
# Run surge with --domain flag
|
|
135
|
+
cmd = [*surge_cmd, tmpdir, "--domain", domain]
|
|
136
|
+
result = subprocess.run(
|
|
137
|
+
cmd,
|
|
138
|
+
capture_output=True,
|
|
139
|
+
text=True,
|
|
140
|
+
timeout=60,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if result.returncode != 0:
|
|
144
|
+
error_msg = result.stderr or result.stdout or "Unknown error"
|
|
145
|
+
raise RuntimeError(f"Surge deployment failed: {error_msg}")
|
|
146
|
+
|
|
147
|
+
return f"https://{domain}"
|
|
148
|
+
|
|
149
|
+
def _build_html(self, agent: Agent) -> str:
|
|
150
|
+
profile = agent.profile
|
|
151
|
+
system_prompt = (profile.system_prompt if profile else "") or ""
|
|
152
|
+
tools = profile.tools if profile else []
|
|
153
|
+
model_name = profile.llm_client.model_name if profile else "unknown"
|
|
154
|
+
return build_export_html(agent.session, system_prompt, tools, model_name)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit.styles import Style
|
|
7
|
+
|
|
8
|
+
from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
|
|
9
|
+
from klaude_code.protocol import commands, events, model
|
|
10
|
+
from klaude_code.ui.modes.repl.clipboard import copy_to_clipboard
|
|
11
|
+
from klaude_code.ui.terminal.selector import SelectItem, select_one
|
|
12
|
+
|
|
13
|
+
FORK_SELECT_STYLE = Style(
|
|
14
|
+
[
|
|
15
|
+
("msg", ""),
|
|
16
|
+
("meta", "fg:ansibrightblack"),
|
|
17
|
+
("separator", "fg:ansibrightblack"),
|
|
18
|
+
("assistant", "fg:ansiblue"),
|
|
19
|
+
("pointer", "bold fg:ansigreen"),
|
|
20
|
+
("search_prefix", "fg:ansibrightblack"),
|
|
21
|
+
("search_success", "noinherit fg:ansigreen"),
|
|
22
|
+
("search_none", "noinherit fg:ansired"),
|
|
23
|
+
("question", "bold"),
|
|
24
|
+
("text", ""),
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ForkPoint:
|
|
31
|
+
"""A fork point in conversation history."""
|
|
32
|
+
|
|
33
|
+
history_index: int | None # None means fork entire conversation
|
|
34
|
+
user_message: str
|
|
35
|
+
tool_call_stats: dict[str, int] # tool_name -> count
|
|
36
|
+
last_assistant_summary: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _truncate(text: str, max_len: int = 60) -> str:
|
|
40
|
+
"""Truncate text to max_len, adding ellipsis if needed."""
|
|
41
|
+
text = text.replace("\n", " ").strip()
|
|
42
|
+
if len(text) <= max_len:
|
|
43
|
+
return text
|
|
44
|
+
return text[: max_len - 3] + "..."
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _build_fork_points(conversation_history: list[model.ConversationItem]) -> list[ForkPoint]:
|
|
48
|
+
"""Build list of fork points from conversation history.
|
|
49
|
+
|
|
50
|
+
Fork points are:
|
|
51
|
+
- Each UserMessageItem position (for UI display, including first which would be empty session)
|
|
52
|
+
- The end of the conversation (fork entire conversation)
|
|
53
|
+
"""
|
|
54
|
+
fork_points: list[ForkPoint] = []
|
|
55
|
+
user_indices: list[int] = []
|
|
56
|
+
|
|
57
|
+
for i, item in enumerate(conversation_history):
|
|
58
|
+
if isinstance(item, model.UserMessageItem):
|
|
59
|
+
user_indices.append(i)
|
|
60
|
+
|
|
61
|
+
# For each UserMessageItem, create a fork point at that position
|
|
62
|
+
for i, user_idx in enumerate(user_indices):
|
|
63
|
+
user_item = conversation_history[user_idx]
|
|
64
|
+
assert isinstance(user_item, model.UserMessageItem)
|
|
65
|
+
|
|
66
|
+
# Find the end of this "task" (next UserMessageItem or end of history)
|
|
67
|
+
next_user_idx = user_indices[i + 1] if i + 1 < len(user_indices) else len(conversation_history)
|
|
68
|
+
|
|
69
|
+
# Count tool calls by name and find last assistant message in this segment
|
|
70
|
+
tool_stats: dict[str, int] = {}
|
|
71
|
+
last_assistant_content = ""
|
|
72
|
+
for j in range(user_idx, next_user_idx):
|
|
73
|
+
item = conversation_history[j]
|
|
74
|
+
if isinstance(item, model.ToolCallItem):
|
|
75
|
+
tool_stats[item.name] = tool_stats.get(item.name, 0) + 1
|
|
76
|
+
elif isinstance(item, model.AssistantMessageItem) and item.content:
|
|
77
|
+
last_assistant_content = item.content
|
|
78
|
+
|
|
79
|
+
fork_points.append(
|
|
80
|
+
ForkPoint(
|
|
81
|
+
history_index=user_idx,
|
|
82
|
+
user_message=user_item.content or "(empty)",
|
|
83
|
+
tool_call_stats=tool_stats,
|
|
84
|
+
last_assistant_summary=_truncate(last_assistant_content) if last_assistant_content else "",
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Add the "fork entire conversation" option at the end
|
|
89
|
+
if user_indices:
|
|
90
|
+
fork_points.append(
|
|
91
|
+
ForkPoint(
|
|
92
|
+
history_index=None, # None means fork entire conversation
|
|
93
|
+
user_message="", # No specific message, this represents the end
|
|
94
|
+
tool_call_stats={},
|
|
95
|
+
last_assistant_summary="",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return fork_points
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _build_select_items(fork_points: list[ForkPoint]) -> list[SelectItem[int | None]]:
|
|
103
|
+
"""Build SelectItem list from fork points."""
|
|
104
|
+
items: list[SelectItem[int | None]] = []
|
|
105
|
+
|
|
106
|
+
for i, fp in enumerate(fork_points):
|
|
107
|
+
is_first = i == 0
|
|
108
|
+
is_last = i == len(fork_points) - 1
|
|
109
|
+
|
|
110
|
+
# Build the title
|
|
111
|
+
title_parts: list[tuple[str, str]] = []
|
|
112
|
+
|
|
113
|
+
# First line: separator (with special markers for first/last fork points)
|
|
114
|
+
if is_first and not is_last:
|
|
115
|
+
title_parts.append(("class:separator", "----- fork from here (empty session) -----\n\n"))
|
|
116
|
+
elif is_last:
|
|
117
|
+
title_parts.append(("class:separator", "----- fork from here (entire session) -----\n\n"))
|
|
118
|
+
else:
|
|
119
|
+
title_parts.append(("class:separator", "----- fork from here -----\n\n"))
|
|
120
|
+
|
|
121
|
+
if not is_last:
|
|
122
|
+
# Second line: user message
|
|
123
|
+
title_parts.append(("class:msg", f"user: {_truncate(fp.user_message, 70)}\n"))
|
|
124
|
+
|
|
125
|
+
# Third line: tool call stats (if any)
|
|
126
|
+
if fp.tool_call_stats:
|
|
127
|
+
tool_parts = [f"{name} × {count}" for name, count in fp.tool_call_stats.items()]
|
|
128
|
+
title_parts.append(("class:meta", f"tools: {', '.join(tool_parts)}\n"))
|
|
129
|
+
|
|
130
|
+
# Fourth line: last assistant message summary (if any)
|
|
131
|
+
if fp.last_assistant_summary:
|
|
132
|
+
title_parts.append(("class:assistant", f"ai: {fp.last_assistant_summary}\n"))
|
|
133
|
+
|
|
134
|
+
# Empty line at the end
|
|
135
|
+
title_parts.append(("class:text", "\n"))
|
|
136
|
+
|
|
137
|
+
items.append(
|
|
138
|
+
SelectItem(
|
|
139
|
+
title=title_parts,
|
|
140
|
+
value=fp.history_index,
|
|
141
|
+
search_text=fp.user_message if not is_last else "fork entire conversation",
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return items
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _select_fork_point_sync(fork_points: list[ForkPoint]) -> int | None | Literal["cancelled"]:
|
|
149
|
+
"""Interactive fork point selection (sync version for asyncio.to_thread).
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
- int: history index to fork at (exclusive)
|
|
153
|
+
- None: fork entire conversation
|
|
154
|
+
- "cancelled": user cancelled selection
|
|
155
|
+
"""
|
|
156
|
+
items = _build_select_items(fork_points)
|
|
157
|
+
if not items:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
# Default to the last option (fork entire conversation)
|
|
161
|
+
last_value = items[-1].value
|
|
162
|
+
|
|
163
|
+
# Non-interactive environments default to forking entire conversation
|
|
164
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
165
|
+
return last_value
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
result = select_one(
|
|
169
|
+
message="Select fork point (messages before this point will be included):",
|
|
170
|
+
items=items,
|
|
171
|
+
pointer="→",
|
|
172
|
+
style=FORK_SELECT_STYLE,
|
|
173
|
+
initial_value=last_value,
|
|
174
|
+
highlight_pointed_item=False,
|
|
175
|
+
)
|
|
176
|
+
if result is None:
|
|
177
|
+
return "cancelled"
|
|
178
|
+
return result
|
|
179
|
+
except KeyboardInterrupt:
|
|
180
|
+
return "cancelled"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ForkSessionCommand(CommandABC):
|
|
184
|
+
"""Fork current session to a new session id and show a resume command."""
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def name(self) -> commands.CommandName:
|
|
188
|
+
return commands.CommandName.FORK_SESSION
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def summary(self) -> str:
|
|
192
|
+
return "Fork the current session and show a resume-by-id command"
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def is_interactive(self) -> bool:
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
|
|
199
|
+
del user_input # unused
|
|
200
|
+
|
|
201
|
+
if agent.session.messages_count == 0:
|
|
202
|
+
event = events.DeveloperMessageEvent(
|
|
203
|
+
session_id=agent.session.id,
|
|
204
|
+
item=model.DeveloperMessageItem(
|
|
205
|
+
content="(no messages to fork)",
|
|
206
|
+
command_output=model.CommandOutput(command_name=self.name),
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
return CommandResult(events=[event], persist_user_input=False, persist_events=False)
|
|
210
|
+
|
|
211
|
+
# Build fork points from conversation history
|
|
212
|
+
fork_points = _build_fork_points(agent.session.conversation_history)
|
|
213
|
+
|
|
214
|
+
if not fork_points:
|
|
215
|
+
# Only one user message, just fork entirely
|
|
216
|
+
new_session = agent.session.fork()
|
|
217
|
+
await new_session.wait_for_flush()
|
|
218
|
+
|
|
219
|
+
resume_cmd = f"klaude --resume-by-id {new_session.id}"
|
|
220
|
+
copy_to_clipboard(resume_cmd)
|
|
221
|
+
|
|
222
|
+
event = events.DeveloperMessageEvent(
|
|
223
|
+
session_id=agent.session.id,
|
|
224
|
+
item=model.DeveloperMessageItem(
|
|
225
|
+
content=f"Session forked successfully. New session id: {new_session.id}",
|
|
226
|
+
command_output=model.CommandOutput(
|
|
227
|
+
command_name=self.name,
|
|
228
|
+
ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
return CommandResult(events=[event], persist_user_input=False, persist_events=False)
|
|
233
|
+
|
|
234
|
+
# Interactive selection
|
|
235
|
+
selected = await asyncio.to_thread(_select_fork_point_sync, fork_points)
|
|
236
|
+
|
|
237
|
+
if selected == "cancelled":
|
|
238
|
+
event = events.DeveloperMessageEvent(
|
|
239
|
+
session_id=agent.session.id,
|
|
240
|
+
item=model.DeveloperMessageItem(
|
|
241
|
+
content="(fork cancelled)",
|
|
242
|
+
command_output=model.CommandOutput(command_name=self.name),
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
return CommandResult(events=[event], persist_user_input=False, persist_events=False)
|
|
246
|
+
|
|
247
|
+
# Perform the fork
|
|
248
|
+
new_session = agent.session.fork(until_index=selected)
|
|
249
|
+
await new_session.wait_for_flush()
|
|
250
|
+
|
|
251
|
+
# Build result message
|
|
252
|
+
fork_description = "entire conversation" if selected is None else f"up to message index {selected}"
|
|
253
|
+
|
|
254
|
+
resume_cmd = f"klaude --resume-by-id {new_session.id}"
|
|
255
|
+
copy_to_clipboard(resume_cmd)
|
|
256
|
+
|
|
257
|
+
event = events.DeveloperMessageEvent(
|
|
258
|
+
session_id=agent.session.id,
|
|
259
|
+
item=model.DeveloperMessageItem(
|
|
260
|
+
content=f"Session forked ({fork_description}). New session id: {new_session.id}",
|
|
261
|
+
command_output=model.CommandOutput(
|
|
262
|
+
command_name=self.name,
|
|
263
|
+
ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
|
|
264
|
+
),
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
return CommandResult(events=[event], persist_user_input=False, persist_events=False)
|
klaude_code/command/help_cmd.py
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
from klaude_code.command.command_abc import CommandABC, CommandResult
|
|
2
|
-
from klaude_code.command.registry import register_command
|
|
3
|
-
from klaude_code.core.agent import Agent
|
|
1
|
+
from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
|
|
4
2
|
from klaude_code.protocol import commands, events, model
|
|
5
3
|
|
|
6
4
|
|
|
7
|
-
@register_command
|
|
8
5
|
class HelpCommand(CommandABC):
|
|
9
6
|
"""Display help information for all available slash commands."""
|
|
10
7
|
|
|
@@ -16,7 +13,8 @@ class HelpCommand(CommandABC):
|
|
|
16
13
|
def summary(self) -> str:
|
|
17
14
|
return "Show help and available commands"
|
|
18
15
|
|
|
19
|
-
async def run(self,
|
|
16
|
+
async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
|
|
17
|
+
del user_input # unused
|
|
20
18
|
lines: list[str] = [
|
|
21
19
|
"""
|
|
22
20
|
Usage:
|
|
@@ -24,8 +22,9 @@ Usage:
|
|
|
24
22
|
[b]esc[/b] to interrupt agent task
|
|
25
23
|
[b]shift-enter[/b] or [b]ctrl-j[/b] for new line
|
|
26
24
|
[b]ctrl-v[/b] for pasting image
|
|
25
|
+
[b]ctrl-l[/b] to switch model
|
|
26
|
+
[b]ctrl-t[/b] to switch thinking level
|
|
27
27
|
[b]--continue[/b] or [b]--resume[/b] to continue an old session
|
|
28
|
-
[b]--select-model[/b] to switch model
|
|
29
28
|
|
|
30
29
|
Available slash commands:"""
|
|
31
30
|
]
|
|
@@ -37,8 +36,8 @@ Available slash commands:"""
|
|
|
37
36
|
|
|
38
37
|
if commands:
|
|
39
38
|
for cmd_name, cmd_obj in sorted(commands.items()):
|
|
40
|
-
|
|
41
|
-
lines.append(f" [b]/{cmd_name}[/b]{
|
|
39
|
+
placeholder = f" \\[{cmd_obj.placeholder}]" if cmd_obj.support_addition_params else ""
|
|
40
|
+
lines.append(f" [b]/{cmd_name}[/b]{placeholder} — {cmd_obj.summary}")
|
|
42
41
|
|
|
43
42
|
event = events.DeveloperMessageEvent(
|
|
44
43
|
session_id=agent.session.id,
|
klaude_code/command/model_cmd.py
CHANGED
|
@@ -1,13 +1,47 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
from klaude_code.
|
|
6
|
-
from klaude_code.
|
|
7
|
-
from klaude_code.protocol import commands, events, model
|
|
3
|
+
from prompt_toolkit.styles import Style
|
|
4
|
+
|
|
5
|
+
from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
|
|
6
|
+
from klaude_code.command.model_select import select_model_interactive
|
|
7
|
+
from klaude_code.protocol import commands, events, model, op
|
|
8
|
+
from klaude_code.ui.terminal.selector import SelectItem, select_one
|
|
9
|
+
|
|
10
|
+
SELECT_STYLE = Style(
|
|
11
|
+
[
|
|
12
|
+
("instruction", "ansibrightblack"),
|
|
13
|
+
("pointer", "ansigreen"),
|
|
14
|
+
("highlighted", "ansigreen"),
|
|
15
|
+
("text", "ansibrightblack"),
|
|
16
|
+
("question", "bold"),
|
|
17
|
+
]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _confirm_change_default_model_sync(selected_model: str) -> bool:
|
|
22
|
+
items: list[SelectItem[bool]] = [
|
|
23
|
+
SelectItem(title=[("class:text", "No (session only)\n")], value=False, search_text="No"),
|
|
24
|
+
SelectItem(
|
|
25
|
+
title=[("class:text", "Yes (save as default main_model in ~/.klaude/klaude-config.yaml)\n")],
|
|
26
|
+
value=True,
|
|
27
|
+
search_text="Yes",
|
|
28
|
+
),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
result = select_one(
|
|
33
|
+
message=f"Save '{selected_model}' as default model?",
|
|
34
|
+
items=items,
|
|
35
|
+
pointer="→",
|
|
36
|
+
style=SELECT_STYLE,
|
|
37
|
+
use_search_filter=False,
|
|
38
|
+
)
|
|
39
|
+
except KeyboardInterrupt:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
return bool(result)
|
|
8
43
|
|
|
9
44
|
|
|
10
|
-
@register_command
|
|
11
45
|
class ModelCommand(CommandABC):
|
|
12
46
|
"""Display or change the model configuration."""
|
|
13
47
|
|
|
@@ -23,8 +57,16 @@ class ModelCommand(CommandABC):
|
|
|
23
57
|
def is_interactive(self) -> bool:
|
|
24
58
|
return True
|
|
25
59
|
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
@property
|
|
61
|
+
def support_addition_params(self) -> bool:
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def placeholder(self) -> str:
|
|
66
|
+
return "model name"
|
|
67
|
+
|
|
68
|
+
async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
|
|
69
|
+
selected_model = await asyncio.to_thread(select_model_interactive, preferred=user_input.text)
|
|
28
70
|
|
|
29
71
|
current_model = agent.profile.llm_client.model_name if agent.profile else None
|
|
30
72
|
if selected_model is None or selected_model == current_model:
|
|
@@ -39,5 +81,13 @@ class ModelCommand(CommandABC):
|
|
|
39
81
|
)
|
|
40
82
|
]
|
|
41
83
|
)
|
|
42
|
-
|
|
43
|
-
return CommandResult(
|
|
84
|
+
save_as_default = await asyncio.to_thread(_confirm_change_default_model_sync, selected_model)
|
|
85
|
+
return CommandResult(
|
|
86
|
+
operations=[
|
|
87
|
+
op.ChangeModelOperation(
|
|
88
|
+
session_id=agent.session.id,
|
|
89
|
+
model_name=selected_model,
|
|
90
|
+
save_as_default=save_as_default,
|
|
91
|
+
)
|
|
92
|
+
]
|
|
93
|
+
)
|