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,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
|
+
)
|