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.
Files changed (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. 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
+ ]