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,245 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+
7
+ from klaude_code.trace import log_debug
8
+
9
+
10
+ @dataclass
11
+ class Skill:
12
+ """Skill data structure"""
13
+
14
+ name: str # Skill identifier (lowercase-hyphen)
15
+ description: str # What the skill does and when to use it
16
+ content: str # Full markdown instructions
17
+ location: str # Skill location: 'user' or 'project'
18
+ license: str | None = None
19
+ allowed_tools: list[str] | None = None
20
+ metadata: dict[str, str] | None = None
21
+ skill_path: Path | None = None
22
+
23
+ def to_prompt(self) -> str:
24
+ """Convert skill to prompt format for agent consumption"""
25
+ return f"""# Skill: {self.name}
26
+
27
+ {self.description}
28
+
29
+ ---
30
+
31
+ {self.content}
32
+ """
33
+
34
+
35
+ class SkillLoader:
36
+ """Load and manage Claude Skills from SKILL.md files"""
37
+
38
+ # User-level skills directories (checked in order, later ones override earlier ones with same name)
39
+ USER_SKILLS_DIRS = [
40
+ Path("~/.claude/skills"),
41
+ Path("~/.klaude/skills"),
42
+ Path("~/.claude/plugins/marketplaces"),
43
+ ]
44
+ # Project-level skills directory
45
+ PROJECT_SKILLS_DIR = Path("./.claude/skills")
46
+
47
+ def __init__(self) -> None:
48
+ """Initialize the skill loader"""
49
+ self.loaded_skills: dict[str, Skill] = {}
50
+
51
+ def load_skill(self, skill_path: Path, location: str) -> Skill | None:
52
+ """Load single skill from SKILL.md file
53
+
54
+ Args:
55
+ skill_path: Path to SKILL.md file
56
+ location: Skill location ('user' or 'project')
57
+
58
+ Returns:
59
+ Skill object or None if loading failed
60
+ """
61
+ if not skill_path.exists():
62
+ return None
63
+
64
+ try:
65
+ content = skill_path.read_text(encoding="utf-8")
66
+
67
+ # Parse YAML frontmatter
68
+ frontmatter: dict[str, object] = {}
69
+ markdown_content = content
70
+
71
+ if content.startswith("---"):
72
+ parts = content.split("---", 2)
73
+ if len(parts) >= 3:
74
+ loaded: object = yaml.safe_load(parts[1])
75
+ if isinstance(loaded, dict):
76
+ frontmatter = dict(loaded) # type: ignore[arg-type]
77
+ markdown_content = parts[2].strip()
78
+
79
+ # Extract skill metadata
80
+ name = str(frontmatter.get("name", ""))
81
+ description = str(frontmatter.get("description", ""))
82
+
83
+ if not name or not description:
84
+ return None
85
+
86
+ # Process relative paths in content
87
+ skill_dir = skill_path.parent
88
+ processed_content = self._process_skill_paths(markdown_content, skill_dir)
89
+
90
+ # Create Skill object
91
+ license_val = frontmatter.get("license")
92
+ allowed_tools_val = frontmatter.get("allowed-tools")
93
+ metadata_val = frontmatter.get("metadata")
94
+
95
+ # Convert allowed_tools
96
+ allowed_tools: list[str] | None = None
97
+ if isinstance(allowed_tools_val, list):
98
+ allowed_tools = [str(t) for t in allowed_tools_val] # type: ignore[misc]
99
+
100
+ # Convert metadata
101
+ metadata: dict[str, str] | None = None
102
+ if isinstance(metadata_val, dict):
103
+ metadata = {str(k): str(v) for k, v in metadata_val.items()} # type: ignore[misc]
104
+
105
+ skill = Skill(
106
+ name=name,
107
+ description=description,
108
+ content=processed_content,
109
+ location=location,
110
+ license=str(license_val) if license_val is not None else None,
111
+ allowed_tools=allowed_tools,
112
+ metadata=metadata,
113
+ skill_path=skill_path,
114
+ )
115
+
116
+ return skill
117
+
118
+ except Exception:
119
+ return None
120
+
121
+ def discover_skills(self) -> list[Skill]:
122
+ """Recursively find all SKILL.md files and load them from both user and project directories
123
+
124
+ Returns:
125
+ List of successfully loaded Skill objects
126
+ """
127
+ skills: list[Skill] = []
128
+
129
+ # Load user-level skills from all directories
130
+ for user_dir in self.USER_SKILLS_DIRS:
131
+ expanded_dir = user_dir.expanduser()
132
+ if expanded_dir.exists():
133
+ for skill_file in expanded_dir.rglob("SKILL.md"):
134
+ skill = self.load_skill(skill_file, location="user")
135
+ if skill:
136
+ skills.append(skill)
137
+ self.loaded_skills[skill.name] = skill
138
+
139
+ # Load project-level skills (override user skills if same name)
140
+ project_dir = self.PROJECT_SKILLS_DIR.resolve()
141
+ if project_dir.exists():
142
+ for skill_file in project_dir.rglob("SKILL.md"):
143
+ skill = self.load_skill(skill_file, location="project")
144
+ if skill:
145
+ skills.append(skill)
146
+ self.loaded_skills[skill.name] = skill
147
+
148
+ # Log discovery summary
149
+ if skills:
150
+ user_count = sum(1 for s in skills if s.location == "user")
151
+ project_count = sum(1 for s in skills if s.location == "project")
152
+ parts: list[str] = []
153
+ if user_count > 0:
154
+ parts.append(f"{user_count} user")
155
+ if project_count > 0:
156
+ parts.append(f"{project_count} project")
157
+ log_debug(f"Discovered {len(skills)} Claude Skills ({', '.join(parts)})")
158
+
159
+ return skills
160
+
161
+ def get_skill(self, name: str) -> Skill | None:
162
+ """Get loaded skill by name
163
+
164
+ Args:
165
+ name: Skill name (supports both 'skill-name' and 'namespace:skill-name')
166
+
167
+ Returns:
168
+ Skill object or None if not found
169
+ """
170
+ # Support both formats: 'pdf' and 'document-skills:pdf'
171
+ if ":" in name:
172
+ name = name.split(":")[-1]
173
+
174
+ return self.loaded_skills.get(name)
175
+
176
+ def list_skills(self) -> list[str]:
177
+ """Get list of all loaded skill names"""
178
+ return list(self.loaded_skills.keys())
179
+
180
+ def get_skills_xml(self) -> str:
181
+ """Generate Level 1 metadata in XML format for tool description
182
+
183
+ Returns:
184
+ XML string with all skill metadata
185
+ """
186
+ xml_parts: list[str] = []
187
+ for skill in self.loaded_skills.values():
188
+ xml_parts.append(f"""<skill>
189
+ <name>{skill.name}</name>
190
+ <description>{skill.description}</description>
191
+ <location>{skill.location}</location>
192
+ </skill>""")
193
+ return "\n".join(xml_parts)
194
+
195
+ def _process_skill_paths(self, content: str, skill_dir: Path) -> str:
196
+ """Convert relative paths to absolute paths for Level 3+
197
+
198
+ Supports:
199
+ - scripts/, examples/, templates/, reference/ directories
200
+ - Markdown document references
201
+ - Markdown links [text](path)
202
+
203
+ Args:
204
+ content: Original skill content
205
+ skill_dir: Directory containing the SKILL.md file
206
+
207
+ Returns:
208
+ Content with absolute paths
209
+ """
210
+ # Pattern 1: Directory-based paths (scripts/, examples/, etc.)
211
+ # e.g., "python scripts/generate.py" -> "python /abs/path/to/scripts/generate.py"
212
+ dir_pattern = r"\b(scripts|examples|templates|reference)/([^\s\)]+)"
213
+
214
+ def replace_dir_path(match: re.Match[str]) -> str:
215
+ directory = match.group(1)
216
+ filename = match.group(2)
217
+ abs_path = skill_dir / directory / filename
218
+ return str(abs_path)
219
+
220
+ content = re.sub(dir_pattern, replace_dir_path, content)
221
+
222
+ # Pattern 2: Markdown links [text](./path or path)
223
+ # e.g., "[Guide](./docs/guide.md)" -> "[Guide](`/abs/path/to/docs/guide.md`) (use read_file to access)"
224
+ link_pattern = r"\[([^\]]+)\]\((\./)?([^\)]+\.md)\)"
225
+
226
+ def replace_link(match: re.Match[str]) -> str:
227
+ text = match.group(1)
228
+ filename = match.group(3)
229
+ abs_path = skill_dir / filename
230
+ return f"[{text}](`{abs_path}`) (use read_file to access)"
231
+
232
+ content = re.sub(link_pattern, replace_link, content)
233
+
234
+ # Pattern 3: Standalone markdown references
235
+ # e.g., "see reference.md" -> "see `/abs/path/to/reference.md` (use read_file to access)"
236
+ standalone_pattern = r"(?<!\])\b(\w+\.md)\b(?!\))"
237
+
238
+ def replace_standalone(match: re.Match[str]) -> str:
239
+ filename = match.group(1)
240
+ abs_path = skill_dir / filename
241
+ return f"`{abs_path}` (use read_file to access)"
242
+
243
+ content = re.sub(standalone_pattern, replace_standalone, content)
244
+
245
+ return content
@@ -0,0 +1,24 @@
1
+ Execute a skill within the main conversation
2
+
3
+ <skills_instructions>
4
+ When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
5
+
6
+ How to use skills:
7
+ - Invoke skills using this tool with the skill name only (no arguments)
8
+ - When you invoke a skill, you will see <command-message>The "{name}" skill is loading</command-message>
9
+ - The skill's prompt will expand and provide detailed instructions on how to complete the task
10
+
11
+ Examples:
12
+ - command: "pdf" - invoke the pdf skill
13
+ - command: "xlsx" - invoke the xlsx skill
14
+ - command: "document-skills:pdf" - invoke using fully qualified name
15
+
16
+ Important:
17
+ - Only use skills listed in <available_skills> below
18
+ - Do not invoke a skill that is already running
19
+ - Do not use this tool for built-in CLI commands (like /help, /clear, etc.)
20
+ </skills_instructions>
21
+
22
+ <available_skills>
23
+ $skills_xml
24
+ </available_skills>
@@ -0,0 +1,97 @@
1
+ from pathlib import Path
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from klaude_code.core.tool.memory.skill_loader import SkillLoader
6
+ from klaude_code.core.tool.tool_abc import ToolABC, load_desc
7
+ from klaude_code.core.tool.tool_registry import register
8
+ from klaude_code.protocol import llm_param, model, tools
9
+
10
+
11
+ @register(tools.SKILL)
12
+ class SkillTool(ToolABC):
13
+ """Tool to execute/load a skill within the main conversation"""
14
+
15
+ _skill_loader: SkillLoader | None = None
16
+
17
+ @classmethod
18
+ def set_skill_loader(cls, loader: SkillLoader) -> None:
19
+ """Set the skill loader instance"""
20
+ cls._skill_loader = loader
21
+
22
+ @classmethod
23
+ def schema(cls) -> llm_param.ToolSchema:
24
+ """Generate schema with embedded available skills metadata"""
25
+ skills_xml = cls._generate_skills_xml()
26
+
27
+ return llm_param.ToolSchema(
28
+ name=tools.SKILL,
29
+ type="function",
30
+ description=load_desc(Path(__file__).parent / "skill_tool.md", {"skills_xml": skills_xml}),
31
+ parameters={
32
+ "type": "object",
33
+ "properties": {
34
+ "command": {
35
+ "type": "string",
36
+ "description": "Name of the skill to execute",
37
+ }
38
+ },
39
+ "required": ["command"],
40
+ },
41
+ )
42
+
43
+ @classmethod
44
+ def _generate_skills_xml(cls) -> str:
45
+ """Generate XML format skills metadata"""
46
+ if not cls._skill_loader:
47
+ return ""
48
+
49
+ xml_parts: list[str] = []
50
+ for skill in cls._skill_loader.loaded_skills.values():
51
+ xml_parts.append(f"""<skill>
52
+ <name>{skill.name}</name>
53
+ <description>{skill.description}</description>
54
+ <location>{skill.location}</location>
55
+ </skill>""")
56
+ return "\n".join(xml_parts)
57
+
58
+ class SkillArguments(BaseModel):
59
+ command: str
60
+
61
+ @classmethod
62
+ async def call(cls, arguments: str) -> model.ToolResultItem:
63
+ """Load and return full skill content"""
64
+ try:
65
+ args = cls.SkillArguments.model_validate_json(arguments)
66
+ except ValueError as e:
67
+ return model.ToolResultItem(
68
+ status="error",
69
+ output=f"Invalid arguments: {e}",
70
+ )
71
+
72
+ if not cls._skill_loader:
73
+ return model.ToolResultItem(
74
+ status="error",
75
+ output="Skill loader not initialized",
76
+ )
77
+
78
+ skill = cls._skill_loader.get_skill(args.command)
79
+
80
+ if not skill:
81
+ available = ", ".join(cls._skill_loader.list_skills())
82
+ return model.ToolResultItem(
83
+ status="error",
84
+ output=f"Skill '{args.command}' does not exist. Available skills: {available}",
85
+ )
86
+
87
+ # Get base directory from skill_path
88
+ base_dir = str(skill.skill_path.parent) if skill.skill_path else "unknown"
89
+
90
+ # Return with loading message format
91
+ result = f"""<command-message>The "{skill.name}" skill is running</command-message>
92
+ <command-name>{skill.name}</command-name>
93
+
94
+ Base directory for this skill: {base_dir}
95
+
96
+ {skill.to_prompt()}"""
97
+ return model.ToolResultItem(status="success", output=result)
File without changes
@@ -0,0 +1,43 @@
1
+ Runs a shell command and returns its output.
2
+
3
+ ### Usage Notes
4
+ - When searching for text or files, prefer using `rg`, `rg --files` or `fd` respectively because `rg` and `fd` is much faster than alternatives like `grep` and `find`. (If these command is not found, then use alternatives.)
5
+
6
+ ### Committing changes with git
7
+ Only create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully:
8
+
9
+ Git Safety Protocol:
10
+ - NEVER update the git config
11
+ - NEVER run destructive/irreversible git commands (like push --force, hard reset, etc) unless the user explicitly requests them
12
+ - NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it
13
+ - NEVER run force push to main/master, warn the user if they request it
14
+ - Avoid git commit --amend. ONLY use --amend when either (1) user explicitly requested amend OR (2) adding edits from pre-commit hook (additional instructions below)
15
+ - Before amending: ALWAYS check authorship (git log -1 --format='%an %ae')
16
+ - NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
17
+
18
+ 1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, batch your tool calls together for optimal performance. run the following bash commands in parallel, each using the Bash tool:
19
+ - Run a git status command to see all untracked files.
20
+ - Run a git diff command to see both staged and unstaged changes that will be committed.
21
+ - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
22
+ 2. Analyze all staged changes (both previously staged and newly added) and draft a commit message:
23
+ - Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.).
24
+ - Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files
25
+ - Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
26
+ - Ensure it accurately reflects the changes and their purpose
27
+ 3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, batch your tool calls together for optimal performance. run the following commands in parallel:
28
+ - Add relevant untracked files to the staging area.
29
+ - Run git status to make sure the commit succeeded.
30
+ 4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
31
+
32
+ Important notes:
33
+ - NEVER run additional commands to read or explore code, besides git bash commands
34
+ - NEVER use the TodoWrite or Task tools
35
+ - DO NOT push to the remote repository unless the user explicitly asks you to do so
36
+ - IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
37
+ - If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
38
+ - In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:<example>
39
+ git commit -m "$(cat <<'EOF'
40
+ Commit message here.
41
+ EOF
42
+ )"
43
+ </example>
@@ -0,0 +1,123 @@
1
+ import asyncio
2
+ import re
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from klaude_code import const
9
+ from klaude_code.core.tool.shell.command_safety import is_safe_command
10
+ from klaude_code.core.tool.tool_abc import ToolABC, load_desc
11
+ from klaude_code.core.tool.tool_registry import register
12
+ from klaude_code.protocol import llm_param, model, tools
13
+
14
+ # Regex to strip ANSI escape sequences from command output
15
+ _ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
16
+
17
+
18
+ @register(tools.BASH)
19
+ class BashTool(ToolABC):
20
+ @classmethod
21
+ def schema(cls) -> llm_param.ToolSchema:
22
+ return llm_param.ToolSchema(
23
+ name=tools.BASH,
24
+ type="function",
25
+ description=load_desc(Path(__file__).parent / "bash_tool.md"),
26
+ parameters={
27
+ "type": "object",
28
+ "properties": {
29
+ "command": {
30
+ "type": "string",
31
+ "description": "The bash command to run",
32
+ },
33
+ "timeout_ms": {
34
+ "type": "integer",
35
+ "description": f"The timeout for the command in milliseconds, default is {const.BASH_DEFAULT_TIMEOUT_MS}",
36
+ "default": const.BASH_DEFAULT_TIMEOUT_MS,
37
+ },
38
+ },
39
+ "required": ["command"],
40
+ },
41
+ )
42
+
43
+ class BashArguments(BaseModel):
44
+ command: str
45
+ timeout_ms: int = const.BASH_DEFAULT_TIMEOUT_MS
46
+
47
+ @classmethod
48
+ async def call(cls, arguments: str) -> model.ToolResultItem:
49
+ try:
50
+ args = BashTool.BashArguments.model_validate_json(arguments)
51
+ except ValueError as e:
52
+ return model.ToolResultItem(
53
+ status="error",
54
+ output=f"Invalid arguments: {e}",
55
+ )
56
+ return await cls.call_with_args(args)
57
+
58
+ @classmethod
59
+ async def call_with_args(cls, args: BashArguments) -> model.ToolResultItem:
60
+ # Safety check: only execute commands proven as "known safe"
61
+ result = is_safe_command(args.command)
62
+ if not result.is_safe:
63
+ return model.ToolResultItem(
64
+ status="error",
65
+ output=f"Command rejected: {result.error_msg}",
66
+ )
67
+
68
+ # Run the command using bash -lc so shell semantics work (pipes, &&, etc.)
69
+ # Capture stdout/stderr, respect timeout, and return a ToolMessage.
70
+ cmd = ["bash", "-lc", args.command]
71
+ timeout_sec = max(0.0, args.timeout_ms / 1000.0)
72
+
73
+ try:
74
+ completed = await asyncio.to_thread(
75
+ subprocess.run,
76
+ cmd,
77
+ capture_output=True,
78
+ text=True,
79
+ timeout=timeout_sec,
80
+ check=False,
81
+ )
82
+
83
+ stdout = _ANSI_ESCAPE_RE.sub("", completed.stdout or "")
84
+ stderr = _ANSI_ESCAPE_RE.sub("", completed.stderr or "")
85
+ rc = completed.returncode
86
+
87
+ if rc == 0:
88
+ output = stdout if stdout else ""
89
+ # Include stderr if there is useful diagnostics despite success
90
+ if stderr.strip():
91
+ output = (output + ("\n" if output else "")) + f"[stderr]\n{stderr}"
92
+ return model.ToolResultItem(
93
+ status="success",
94
+ output=output.strip(),
95
+ )
96
+ else:
97
+ combined = ""
98
+ if stdout.strip():
99
+ combined += f"[stdout]\n{stdout}\n"
100
+ if stderr.strip():
101
+ combined += f"[stderr]\n{stderr}"
102
+ if not combined:
103
+ combined = f"Command exited with code {rc}"
104
+ return model.ToolResultItem(
105
+ status="error",
106
+ output=combined.strip(),
107
+ )
108
+
109
+ except subprocess.TimeoutExpired:
110
+ return model.ToolResultItem(
111
+ status="error",
112
+ output=f"Timeout after {args.timeout_ms} ms running: {args.command}",
113
+ )
114
+ except FileNotFoundError:
115
+ return model.ToolResultItem(
116
+ status="error",
117
+ output="bash not found on system path",
118
+ )
119
+ except Exception as e: # safeguard against unexpected failures
120
+ return model.ToolResultItem(
121
+ status="error",
122
+ output=f"Execution error: {e}",
123
+ )