klaude-code 1.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Create an AGENTS.md file with instructions for agent
|
|
3
|
+
from: https://github.com/openai/codex/blob/main/codex-rs/tui/prompt_for_init_command.md
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Generate/Update a file named AGENTS.md that serves as a contributor guide for this repository.
|
|
7
|
+
Your goal is to produce a clear, concise, and well-structured document with descriptive headings and actionable explanations for each section.
|
|
8
|
+
Follow the outline below, but adapt as needed — add sections if relevant, and omit those that do not apply to this project.
|
|
9
|
+
|
|
10
|
+
Document Requirements
|
|
11
|
+
|
|
12
|
+
- Title the document "Repository Guidelines".
|
|
13
|
+
- Use Markdown headings (#, ##, etc.) for structure.
|
|
14
|
+
- Keep the document concise. 200-400 words is optimal.
|
|
15
|
+
- Keep explanations short, direct, and specific to this repository.
|
|
16
|
+
- Provide examples where helpful (commands, directory paths, naming patterns).
|
|
17
|
+
- Maintain a professional, instructional tone.
|
|
18
|
+
|
|
19
|
+
Recommended Sections
|
|
20
|
+
|
|
21
|
+
Project Structure & Module Organization
|
|
22
|
+
|
|
23
|
+
- Outline the project structure, including where the source code, tests, and assets are located.
|
|
24
|
+
|
|
25
|
+
Build, Test, and Development Commands
|
|
26
|
+
|
|
27
|
+
- List key commands for building, testing, and running locally (e.g., npm test, make build).
|
|
28
|
+
- Briefly explain what each command does.
|
|
29
|
+
|
|
30
|
+
Coding Style & Naming Conventions
|
|
31
|
+
|
|
32
|
+
- Specify indentation rules, language-specific style preferences, and naming patterns.
|
|
33
|
+
- Include any formatting or linting tools used.
|
|
34
|
+
|
|
35
|
+
Testing Guidelines
|
|
36
|
+
|
|
37
|
+
- Identify testing frameworks and coverage requirements.
|
|
38
|
+
- State test naming conventions and how to run tests.
|
|
39
|
+
|
|
40
|
+
Commit & Pull Request Guidelines
|
|
41
|
+
|
|
42
|
+
- Summarize commit message conventions found in the project’s Git history.
|
|
43
|
+
- Outline pull request requirements (descriptions, linked issues, screenshots, etc.).
|
|
44
|
+
|
|
45
|
+
(Optional) Add other sections if relevant, such as Security & Configuration Tips, Architecture Overview, or Agent-Specific Instructions.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from importlib.resources import files
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
from klaude_code.command.command_abc import CommandABC, CommandResult, InputAction
|
|
6
|
+
from klaude_code.core.agent import Agent
|
|
7
|
+
from klaude_code.protocol import commands
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PromptCommand(CommandABC):
|
|
11
|
+
"""Command that loads a prompt from a markdown file."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, filename: str, command_name: str | None = None):
|
|
14
|
+
self._filename = filename
|
|
15
|
+
self._command_name = command_name or filename.replace("prompt_", "").replace("prompt-", "").replace(".md", "")
|
|
16
|
+
self._content: str | None = None
|
|
17
|
+
self._metadata: dict[str, str] = {}
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str | commands.CommandName:
|
|
21
|
+
return self._command_name
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def template_name(self) -> str:
|
|
25
|
+
"""filename of the markdown prompt template in the command package."""
|
|
26
|
+
return self._filename
|
|
27
|
+
|
|
28
|
+
def _ensure_loaded(self):
|
|
29
|
+
if self._content is not None:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
raw_text = files("klaude_code.command").joinpath(self.template_name).read_text(encoding="utf-8")
|
|
34
|
+
|
|
35
|
+
if raw_text.startswith("---"):
|
|
36
|
+
parts = raw_text.split("---", 2)
|
|
37
|
+
if len(parts) >= 3:
|
|
38
|
+
self._metadata = yaml.safe_load(parts[1]) or {}
|
|
39
|
+
self._content = parts[2].strip()
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
self._metadata = {}
|
|
43
|
+
self._content = raw_text
|
|
44
|
+
except Exception:
|
|
45
|
+
self._metadata = {"description": "Error loading template"}
|
|
46
|
+
self._content = f"Error loading template: {self.template_name}"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def summary(self) -> str:
|
|
50
|
+
self._ensure_loaded()
|
|
51
|
+
return self._metadata.get("description", f"Execute {self.name} command")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def support_addition_params(self) -> bool:
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
async def run(self, raw: str, agent: Agent) -> CommandResult:
|
|
58
|
+
self._ensure_loaded()
|
|
59
|
+
template_content = self._content or ""
|
|
60
|
+
user_input = raw.strip() or "<none>"
|
|
61
|
+
|
|
62
|
+
if "$ARGUMENTS" in template_content:
|
|
63
|
+
final_prompt = template_content.replace("$ARGUMENTS", user_input)
|
|
64
|
+
else:
|
|
65
|
+
final_prompt = template_content
|
|
66
|
+
if user_input:
|
|
67
|
+
final_prompt += f"\n\nAdditional Instructions:\n{user_input}"
|
|
68
|
+
|
|
69
|
+
return CommandResult(actions=[InputAction.run_agent(final_prompt)])
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
|
4
|
+
from klaude_code.protocol import commands, events
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register_command
|
|
8
|
+
class RefreshTerminalCommand(CommandABC):
|
|
9
|
+
"""Refresh terminal display"""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def name(self) -> commands.CommandName:
|
|
13
|
+
return commands.CommandName.REFRESH_TERMINAL
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def summary(self) -> str:
|
|
17
|
+
return "Refresh terminal display"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def is_interactive(self) -> bool:
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
async def run(self, raw: str, agent: Agent) -> CommandResult:
|
|
24
|
+
import os
|
|
25
|
+
|
|
26
|
+
os.system("cls" if os.name == "nt" else "clear")
|
|
27
|
+
|
|
28
|
+
result = CommandResult(
|
|
29
|
+
events=[
|
|
30
|
+
events.WelcomeEvent(
|
|
31
|
+
work_dir=str(agent.session.work_dir),
|
|
32
|
+
llm_config=agent.get_llm_client().get_llm_config(),
|
|
33
|
+
),
|
|
34
|
+
events.ReplayHistoryEvent(
|
|
35
|
+
session_id=agent.session.id,
|
|
36
|
+
events=list(agent.session.get_history_item()),
|
|
37
|
+
updated_at=agent.session.updated_at,
|
|
38
|
+
is_load=False,
|
|
39
|
+
),
|
|
40
|
+
]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return result
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from importlib.resources import files
|
|
2
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
3
|
+
|
|
4
|
+
from klaude_code.command.command_abc import CommandResult, InputAction
|
|
5
|
+
from klaude_code.command.prompt_command import PromptCommand
|
|
6
|
+
from klaude_code.core.agent import Agent
|
|
7
|
+
from klaude_code.protocol import commands, events, model
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .command_abc import CommandABC
|
|
11
|
+
|
|
12
|
+
_COMMANDS: dict[commands.CommandName | str, "CommandABC"] = {}
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T", bound="CommandABC")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_command(cls: type[T]) -> type[T]:
|
|
18
|
+
"""Decorator to register a command class in the global registry."""
|
|
19
|
+
instance = cls()
|
|
20
|
+
_COMMANDS[instance.name] = instance
|
|
21
|
+
return cls
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_prompt_commands():
|
|
25
|
+
"""Dynamically load prompt-based commands from the command directory."""
|
|
26
|
+
try:
|
|
27
|
+
command_files = files("klaude_code.command").iterdir()
|
|
28
|
+
for file_path in command_files:
|
|
29
|
+
name = file_path.name
|
|
30
|
+
if (name.startswith("prompt_") or name.startswith("prompt-")) and name.endswith(".md"):
|
|
31
|
+
cmd = PromptCommand(name)
|
|
32
|
+
_COMMANDS[cmd.name] = cmd
|
|
33
|
+
except Exception:
|
|
34
|
+
# If resource loading fails, just ignore
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_commands() -> dict[commands.CommandName | str, "CommandABC"]:
|
|
39
|
+
"""Get all registered commands."""
|
|
40
|
+
return _COMMANDS.copy()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_slash_command_name(name: str) -> bool:
|
|
44
|
+
return name in _COMMANDS
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def dispatch_command(raw: str, agent: Agent) -> CommandResult:
|
|
48
|
+
# Detect command name
|
|
49
|
+
if not raw.startswith("/"):
|
|
50
|
+
return CommandResult(actions=[InputAction.run_agent(raw)])
|
|
51
|
+
|
|
52
|
+
splits = raw.split(" ", maxsplit=1)
|
|
53
|
+
command_name_raw = splits[0][1:]
|
|
54
|
+
rest = " ".join(splits[1:]) if len(splits) > 1 else ""
|
|
55
|
+
|
|
56
|
+
# Try to match against registered commands (both Enum and string keys)
|
|
57
|
+
command_key = None
|
|
58
|
+
|
|
59
|
+
# First try exact string match
|
|
60
|
+
if command_name_raw in _COMMANDS:
|
|
61
|
+
command_key = command_name_raw
|
|
62
|
+
else:
|
|
63
|
+
# Then try Enum conversion for standard commands
|
|
64
|
+
try:
|
|
65
|
+
enum_key = commands.CommandName(command_name_raw)
|
|
66
|
+
if enum_key in _COMMANDS:
|
|
67
|
+
command_key = enum_key
|
|
68
|
+
except ValueError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
if command_key is None:
|
|
72
|
+
return CommandResult(actions=[InputAction.run_agent(raw)])
|
|
73
|
+
|
|
74
|
+
command = _COMMANDS[command_key]
|
|
75
|
+
command_identifier: commands.CommandName | str = command.name
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
return await command.run(rest, agent)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
command_output = (
|
|
81
|
+
model.CommandOutput(command_name=command_identifier, is_error=True)
|
|
82
|
+
if isinstance(command_identifier, commands.CommandName)
|
|
83
|
+
else None
|
|
84
|
+
)
|
|
85
|
+
return CommandResult(
|
|
86
|
+
events=[
|
|
87
|
+
events.DeveloperMessageEvent(
|
|
88
|
+
session_id=agent.session.id,
|
|
89
|
+
item=model.DeveloperMessageItem(
|
|
90
|
+
content=f"Command {command_identifier} error: [{e.__class__.__name__}] {str(e)}",
|
|
91
|
+
command_output=command_output,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def has_interactive_command(raw: str) -> bool:
|
|
99
|
+
if not raw.startswith("/"):
|
|
100
|
+
return False
|
|
101
|
+
splits = raw.split(" ", maxsplit=1)
|
|
102
|
+
command_name_raw = splits[0][1:]
|
|
103
|
+
try:
|
|
104
|
+
command_name = commands.CommandName(command_name_raw)
|
|
105
|
+
except ValueError:
|
|
106
|
+
return False
|
|
107
|
+
if command_name not in _COMMANDS:
|
|
108
|
+
return False
|
|
109
|
+
command = _COMMANDS[command_name]
|
|
110
|
+
return command.is_interactive
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
|
4
|
+
from klaude_code.protocol import commands, events, model
|
|
5
|
+
from klaude_code.session.session import Session
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def accumulate_session_usage(session: Session) -> tuple[model.Usage, int]:
|
|
9
|
+
"""Accumulate usage statistics from all ResponseMetadataItems in session history.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
A tuple of (accumulated_usage, task_count)
|
|
13
|
+
"""
|
|
14
|
+
total = model.Usage()
|
|
15
|
+
task_count = 0
|
|
16
|
+
|
|
17
|
+
for item in session.conversation_history:
|
|
18
|
+
if isinstance(item, model.ResponseMetadataItem) and item.usage:
|
|
19
|
+
task_count += 1
|
|
20
|
+
usage = item.usage
|
|
21
|
+
total.input_tokens += usage.input_tokens
|
|
22
|
+
total.cached_tokens += usage.cached_tokens
|
|
23
|
+
total.reasoning_tokens += usage.reasoning_tokens
|
|
24
|
+
total.output_tokens += usage.output_tokens
|
|
25
|
+
total.total_tokens += usage.total_tokens
|
|
26
|
+
|
|
27
|
+
# Accumulate costs
|
|
28
|
+
if usage.input_cost is not None:
|
|
29
|
+
total.input_cost = (total.input_cost or 0.0) + usage.input_cost
|
|
30
|
+
if usage.output_cost is not None:
|
|
31
|
+
total.output_cost = (total.output_cost or 0.0) + usage.output_cost
|
|
32
|
+
if usage.cache_read_cost is not None:
|
|
33
|
+
total.cache_read_cost = (total.cache_read_cost or 0.0) + usage.cache_read_cost
|
|
34
|
+
if usage.total_cost is not None:
|
|
35
|
+
total.total_cost = (total.total_cost or 0.0) + usage.total_cost
|
|
36
|
+
|
|
37
|
+
# Keep the latest context_usage_percent
|
|
38
|
+
if usage.context_usage_percent is not None:
|
|
39
|
+
total.context_usage_percent = usage.context_usage_percent
|
|
40
|
+
|
|
41
|
+
return total, task_count
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _format_tokens(tokens: int) -> str:
|
|
45
|
+
"""Format token count with K/M suffix for readability."""
|
|
46
|
+
if tokens >= 1_000_000:
|
|
47
|
+
return f"{tokens / 1_000_000:.2f}M"
|
|
48
|
+
if tokens >= 1_000:
|
|
49
|
+
return f"{tokens / 1_000:.1f}K"
|
|
50
|
+
return str(tokens)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _format_cost(cost: float | None) -> str:
|
|
54
|
+
"""Format cost in USD."""
|
|
55
|
+
if cost is None:
|
|
56
|
+
return "-"
|
|
57
|
+
if cost < 0.01:
|
|
58
|
+
return f"${cost:.4f}"
|
|
59
|
+
return f"${cost:.2f}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def format_status_content(usage: model.Usage) -> str:
|
|
63
|
+
"""Format session status as comma-separated text."""
|
|
64
|
+
parts: list[str] = []
|
|
65
|
+
|
|
66
|
+
parts.append(f"Input: {_format_tokens(usage.input_tokens)}")
|
|
67
|
+
if usage.cached_tokens > 0:
|
|
68
|
+
parts.append(f"Cached: {_format_tokens(usage.cached_tokens)}")
|
|
69
|
+
parts.append(f"Output: {_format_tokens(usage.output_tokens)}")
|
|
70
|
+
parts.append(f"Total: {_format_tokens(usage.total_tokens)}")
|
|
71
|
+
|
|
72
|
+
if usage.total_cost is not None:
|
|
73
|
+
parts.append(f"Cost: {_format_cost(usage.total_cost)}")
|
|
74
|
+
|
|
75
|
+
return ", ".join(parts)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@register_command
|
|
79
|
+
class StatusCommand(CommandABC):
|
|
80
|
+
"""Display session usage statistics."""
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def name(self) -> commands.CommandName:
|
|
84
|
+
return commands.CommandName.STATUS
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def summary(self) -> str:
|
|
88
|
+
return "Show session usage statistics"
|
|
89
|
+
|
|
90
|
+
async def run(self, raw: str, agent: Agent) -> CommandResult:
|
|
91
|
+
session = agent.session
|
|
92
|
+
usage, task_count = accumulate_session_usage(session)
|
|
93
|
+
|
|
94
|
+
event = events.DeveloperMessageEvent(
|
|
95
|
+
session_id=session.id,
|
|
96
|
+
item=model.DeveloperMessageItem(
|
|
97
|
+
content=format_status_content(usage),
|
|
98
|
+
command_output=model.CommandOutput(
|
|
99
|
+
command_name=self.name,
|
|
100
|
+
ui_extra=model.ToolResultUIExtra(
|
|
101
|
+
type=model.ToolResultUIExtraType.SESSION_STATUS,
|
|
102
|
+
session_status=model.SessionStatusUIExtra(
|
|
103
|
+
usage=usage,
|
|
104
|
+
task_count=task_count,
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return CommandResult(events=[event])
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from klaude_code.command.command_abc import CommandABC, CommandResult
|
|
6
|
+
from klaude_code.command.registry import register_command
|
|
7
|
+
from klaude_code.core.agent import Agent
|
|
8
|
+
from klaude_code.protocol import commands, events, model
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register_command
|
|
12
|
+
class TerminalSetupCommand(CommandABC):
|
|
13
|
+
"""Setup shift+enter newline functionality in terminal"""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def name(self) -> commands.CommandName:
|
|
17
|
+
return commands.CommandName.TERMINAL_SETUP
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def summary(self) -> str:
|
|
21
|
+
return "Install shift+enter key binding for newlines"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_interactive(self) -> bool:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
async def run(self, raw: str, agent: Agent) -> CommandResult:
|
|
28
|
+
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
if term_program == "ghostty":
|
|
32
|
+
message = self._setup_ghostty()
|
|
33
|
+
elif term_program == "iterm.app":
|
|
34
|
+
message = self._setup_iterm()
|
|
35
|
+
elif term_program == "vscode":
|
|
36
|
+
# VS Code family terminals (VS Code, Windsurf, Cursor) all report TERM_PROGRAM=vscode
|
|
37
|
+
message = self._setup_vscode_family()
|
|
38
|
+
else:
|
|
39
|
+
# Provide generic manual configuration guide for unknown or unsupported terminals
|
|
40
|
+
message = self._setup_generic(term_program)
|
|
41
|
+
|
|
42
|
+
return self._create_success_result(agent, message)
|
|
43
|
+
|
|
44
|
+
except Exception as e:
|
|
45
|
+
return self._create_error_result(agent, f"Error configuring terminal: {str(e)}")
|
|
46
|
+
|
|
47
|
+
def _setup_ghostty(self) -> str:
|
|
48
|
+
"""Configure shift+enter newline for Ghostty terminal"""
|
|
49
|
+
config_dir = Path.home() / ".config" / "ghostty"
|
|
50
|
+
config_file = config_dir / "config"
|
|
51
|
+
|
|
52
|
+
keybind_line = 'keybind="shift+enter=text:\\n"'
|
|
53
|
+
|
|
54
|
+
# Ensure config directory exists
|
|
55
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
# Check if configuration already exists in config file
|
|
58
|
+
if config_file.exists():
|
|
59
|
+
content = config_file.read_text()
|
|
60
|
+
if keybind_line in content or 'keybind="shift+enter=' in content:
|
|
61
|
+
return "Ghostty terminal shift+enter newline configuration already exists"
|
|
62
|
+
|
|
63
|
+
# Add configuration
|
|
64
|
+
with config_file.open("a", encoding="utf-8") as f:
|
|
65
|
+
if config_file.exists() and not config_file.read_text().endswith("\n"):
|
|
66
|
+
f.write("\n")
|
|
67
|
+
f.write(f"{keybind_line}\n")
|
|
68
|
+
|
|
69
|
+
return f"Added shift+enter newline configuration for Ghostty terminal to {config_file}"
|
|
70
|
+
|
|
71
|
+
def _setup_iterm(self) -> str:
|
|
72
|
+
"""Configure shift+enter newline for iTerm terminal using defaults command"""
|
|
73
|
+
try:
|
|
74
|
+
# First check if iTerm preferences exist
|
|
75
|
+
prefs_path = Path.home() / "Library" / "Preferences" / "com.googlecode.iterm2.plist"
|
|
76
|
+
if not prefs_path.exists():
|
|
77
|
+
return "iTerm preferences file not found. Please open iTerm first to create initial preferences."
|
|
78
|
+
|
|
79
|
+
# Check if the key binding already exists
|
|
80
|
+
check_cmd = ["defaults", "read", "com.googlecode.iterm2", "New Bookmarks"]
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
result = subprocess.run(check_cmd, capture_output=True, text=True, check=True)
|
|
84
|
+
# If we can read bookmarks, iTerm is properly configured
|
|
85
|
+
except subprocess.CalledProcessError:
|
|
86
|
+
return "Unable to read iTerm configuration. Please ensure iTerm is properly installed and has been opened at least once."
|
|
87
|
+
|
|
88
|
+
# Add to the default profile's keyboard map
|
|
89
|
+
add_keymap_cmd = [
|
|
90
|
+
"defaults",
|
|
91
|
+
"write",
|
|
92
|
+
"com.googlecode.iterm2",
|
|
93
|
+
"GlobalKeyMap",
|
|
94
|
+
"-dict-add",
|
|
95
|
+
# Do not include quotes when passing args as a list (no shell)
|
|
96
|
+
"0x0d-0x20000",
|
|
97
|
+
# Pass Property List dict directly; \n should be literal backslash-n so iTerm parses newline
|
|
98
|
+
'{Action=12;Text="\\\\n";}',
|
|
99
|
+
]
|
|
100
|
+
# Execute without shell so arguments are passed correctly
|
|
101
|
+
result = subprocess.run(add_keymap_cmd, capture_output=True, text=True)
|
|
102
|
+
print(result.stdout, result.stderr)
|
|
103
|
+
if result.returncode == 0:
|
|
104
|
+
return "Successfully configured Shift+Enter for newline in iTerm. Please restart iTerm for changes to take effect."
|
|
105
|
+
else:
|
|
106
|
+
# Fallback to manual instructions if defaults command fails
|
|
107
|
+
return (
|
|
108
|
+
"Automatic configuration failed. Please manually configure:\n"
|
|
109
|
+
"1. Open iTerm -> Preferences (⌘,)\n"
|
|
110
|
+
"2. Go to Profiles -> Keys -> Key Mappings\n"
|
|
111
|
+
"3. Click '+' to add: Shift+Enter -> Send Text -> \\n"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
raise Exception(f"Error configuring iTerm: {str(e)}")
|
|
116
|
+
|
|
117
|
+
def _setup_vscode_family(self) -> str:
|
|
118
|
+
"""Configure shift+enter newline for VS Code family terminals (VS Code, Windsurf, Cursor).
|
|
119
|
+
|
|
120
|
+
These editors share TERM_PROGRAM=vscode and use keybindings.json under their respective
|
|
121
|
+
Application Support folders. We ensure the required keybinding exists; if not, we append it.
|
|
122
|
+
"""
|
|
123
|
+
base_dir = Path.home() / "Library" / "Application Support"
|
|
124
|
+
targets = [
|
|
125
|
+
("VS Code", base_dir / "Code" / "User" / "keybindings.json"),
|
|
126
|
+
("Windsurf", base_dir / "Windsurf" / "User" / "keybindings.json"),
|
|
127
|
+
("Cursor", base_dir / "Cursor" / "User" / "keybindings.json"),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
mapping_block = r""" {
|
|
131
|
+
"key": "shift+enter",
|
|
132
|
+
"command": "workbench.action.terminal.sendSequence",
|
|
133
|
+
"args": {
|
|
134
|
+
"text": "\\\r\n"
|
|
135
|
+
},
|
|
136
|
+
"when": "terminalFocus"
|
|
137
|
+
}"""
|
|
138
|
+
|
|
139
|
+
results: list[str] = []
|
|
140
|
+
|
|
141
|
+
for name, file_path in targets:
|
|
142
|
+
try:
|
|
143
|
+
_, msg = self._ensure_vscode_keybinding(file_path, mapping_block)
|
|
144
|
+
results.append(f"{name}: {msg}")
|
|
145
|
+
except Exception as e: # pragma: no cover - protect against any unexpected FS issue
|
|
146
|
+
results.append(f"{name}: failed to update keybindings ({e})")
|
|
147
|
+
|
|
148
|
+
return "\n".join(results)
|
|
149
|
+
|
|
150
|
+
def _ensure_vscode_keybinding(self, path: Path, mapping_block: str) -> tuple[bool, str]:
|
|
151
|
+
"""Ensure the VS Code-style keybinding exists in the given keybindings.json file.
|
|
152
|
+
|
|
153
|
+
Returns (added, message).
|
|
154
|
+
- added=True if we created or modified the file to include the mapping
|
|
155
|
+
- added=False if mapping already present or file couldn't be safely modified
|
|
156
|
+
"""
|
|
157
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
|
|
159
|
+
# If file does not exist, create with the mapping in an array
|
|
160
|
+
if not path.exists():
|
|
161
|
+
content = "[\n " + mapping_block + "\n]\n"
|
|
162
|
+
path.write_text(content, encoding="utf-8")
|
|
163
|
+
return True, f"created {path} with Shift+Enter mapping"
|
|
164
|
+
|
|
165
|
+
# Read existing content
|
|
166
|
+
raw = path.read_text(encoding="utf-8")
|
|
167
|
+
text = raw
|
|
168
|
+
|
|
169
|
+
# Quick detection: if both key and command exist together anywhere, assume configured
|
|
170
|
+
if '"key": "shift+enter"' in text and "workbench.action.terminal.sendSequence" in text:
|
|
171
|
+
return False, "already configured"
|
|
172
|
+
|
|
173
|
+
stripped = text.strip()
|
|
174
|
+
# If file is empty, write a fresh array
|
|
175
|
+
if stripped == "":
|
|
176
|
+
content = "[\n " + mapping_block + "\n]\n"
|
|
177
|
+
path.write_text(content, encoding="utf-8")
|
|
178
|
+
return True, "initialized empty keybindings.json with mapping"
|
|
179
|
+
|
|
180
|
+
# If the content contains a top-level array (allowing header comments), append before the final ]
|
|
181
|
+
open_idx = text.find("[")
|
|
182
|
+
close_idx = text.rfind("]")
|
|
183
|
+
if open_idx != -1 and close_idx != -1 and open_idx < close_idx:
|
|
184
|
+
before = text[:close_idx].rstrip()
|
|
185
|
+
after = text[close_idx:]
|
|
186
|
+
|
|
187
|
+
# Heuristic: treat as non-empty if there's an object marker between [ and ]
|
|
188
|
+
inner = text[open_idx + 1 : close_idx]
|
|
189
|
+
has_item = "{" in inner
|
|
190
|
+
|
|
191
|
+
# Construct new content by adding optional comma, newline, then our block
|
|
192
|
+
new_content = before + ("," if has_item else "") + "\n" + mapping_block + "\n" + after
|
|
193
|
+
|
|
194
|
+
path.write_text(new_content, encoding="utf-8")
|
|
195
|
+
return True, "appended mapping"
|
|
196
|
+
|
|
197
|
+
# Not an array – avoid modifying to prevent corrupting user config
|
|
198
|
+
return (
|
|
199
|
+
False,
|
|
200
|
+
"unsupported keybindings.json format (not an array); please add mapping manually",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def _setup_generic(self, term_program: str) -> str:
|
|
204
|
+
"""Provide generic manual configuration guide for unknown or unsupported terminals"""
|
|
205
|
+
if term_program:
|
|
206
|
+
intro = f"Terminal type '{term_program}' is not specifically supported, but you can manually configure shift+enter newline functionality."
|
|
207
|
+
else:
|
|
208
|
+
intro = "Unable to detect terminal type, but you can manually configure shift+enter newline functionality."
|
|
209
|
+
|
|
210
|
+
message = (
|
|
211
|
+
f"{intro}\n\n"
|
|
212
|
+
"General steps to configure shift+enter for newline:\n"
|
|
213
|
+
"1. Open your terminal's preferences/settings\n"
|
|
214
|
+
"2. Look for 'Key Bindings', 'Key Mappings', or 'Keyboard' section\n"
|
|
215
|
+
"3. Add a new key binding:\n"
|
|
216
|
+
" - Key combination: Shift+Enter\n"
|
|
217
|
+
" - Action: Send text or Insert text\n"
|
|
218
|
+
" - Text to send: \\n (literal newline character)\n"
|
|
219
|
+
"4. Save the configuration\n\n"
|
|
220
|
+
"Note: The exact steps may vary depending on your terminal application. "
|
|
221
|
+
"Currently supported terminals with automatic configuration: Ghostty, iTerm.app, VS Code family (VS Code, Windsurf, Cursor)"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return message
|
|
225
|
+
|
|
226
|
+
def _create_success_result(self, agent: Agent, message: str) -> CommandResult:
|
|
227
|
+
"""Create success result"""
|
|
228
|
+
return CommandResult(
|
|
229
|
+
events=[
|
|
230
|
+
events.DeveloperMessageEvent(
|
|
231
|
+
session_id=agent.session.id,
|
|
232
|
+
item=model.DeveloperMessageItem(
|
|
233
|
+
content=message,
|
|
234
|
+
command_output=model.CommandOutput(command_name=self.name, is_error=False),
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
]
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _create_error_result(self, agent: Agent, message: str) -> CommandResult:
|
|
241
|
+
"""Create error result"""
|
|
242
|
+
return CommandResult(
|
|
243
|
+
events=[
|
|
244
|
+
events.DeveloperMessageEvent(
|
|
245
|
+
session_id=agent.session.id,
|
|
246
|
+
item=model.DeveloperMessageItem(
|
|
247
|
+
content=message,
|
|
248
|
+
command_output=model.CommandOutput(command_name=self.name, is_error=True),
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
]
|
|
252
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .config import Config, config_path, load_config
|
|
2
|
+
from .list_model import display_models_and_providers
|
|
3
|
+
from .select_model import select_model_from_config
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Config",
|
|
7
|
+
"load_config",
|
|
8
|
+
"config_path",
|
|
9
|
+
"display_models_and_providers",
|
|
10
|
+
"select_model_from_config",
|
|
11
|
+
]
|