klaude-code 1.2.21__py3-none-any.whl → 1.2.23__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/cli/debug.py +8 -10
- klaude_code/command/__init__.py +0 -3
- klaude_code/command/status_cmd.py +1 -1
- klaude_code/const/__init__.py +10 -7
- klaude_code/core/manager/sub_agent_manager.py +1 -1
- klaude_code/core/prompt.py +5 -2
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/{prompt-codex-gpt-5-1.md → prompt-codex.md} +9 -42
- klaude_code/core/reminders.py +87 -2
- klaude_code/core/task.py +37 -18
- klaude_code/core/tool/__init__.py +1 -9
- klaude_code/core/tool/file/_utils.py +6 -0
- klaude_code/core/tool/file/apply_patch_tool.py +30 -72
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +35 -18
- klaude_code/core/tool/file/read_tool.py +45 -86
- klaude_code/core/tool/file/write_tool.py +40 -30
- klaude_code/core/tool/shell/bash_tool.py +147 -0
- klaude_code/core/tool/skill/__init__.py +0 -0
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -39
- klaude_code/protocol/commands.py +0 -1
- klaude_code/protocol/model.py +31 -11
- klaude_code/protocol/tools.py +1 -2
- klaude_code/session/export.py +76 -21
- klaude_code/session/store.py +4 -2
- klaude_code/session/templates/export_session.html +28 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +60 -24
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/ui/modes/repl/completers.py +103 -3
- klaude_code/ui/modes/repl/event_handler.py +7 -3
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +42 -3
- klaude_code/ui/renderers/assistant.py +7 -2
- klaude_code/ui/renderers/common.py +26 -11
- klaude_code/ui/renderers/developer.py +12 -5
- klaude_code/ui/renderers/diffs.py +85 -1
- klaude_code/ui/renderers/metadata.py +4 -2
- klaude_code/ui/renderers/thinking.py +1 -1
- klaude_code/ui/renderers/tools.py +75 -129
- klaude_code/ui/renderers/user_input.py +32 -2
- klaude_code/ui/rich/markdown.py +27 -12
- klaude_code/ui/rich/status.py +9 -24
- klaude_code/ui/rich/theme.py +17 -5
- {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/METADATA +19 -13
- {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/RECORD +54 -54
- klaude_code/command/diff_cmd.py +0 -136
- klaude_code/command/prompt-deslop.md +0 -14
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/command/prompt-handoff.md +0 -33
- klaude_code/command/prompt-jj-workspace.md +0 -18
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -175
- klaude_code/core/tool/memory/__init__.py +0 -5
- klaude_code/core/tool/memory/memory_tool.md +0 -20
- klaude_code/core/tool/memory/memory_tool.py +0 -456
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/entry_points.txt +0 -0
|
@@ -15,12 +15,22 @@ class Skill:
|
|
|
15
15
|
name: str # Skill identifier (lowercase-hyphen)
|
|
16
16
|
description: str # What the skill does and when to use it
|
|
17
17
|
content: str # Full markdown instructions
|
|
18
|
-
location: str # Skill location: 'user' or 'project'
|
|
18
|
+
location: str # Skill location: 'system', 'user', or 'project'
|
|
19
19
|
license: str | None = None
|
|
20
20
|
allowed_tools: list[str] | None = None
|
|
21
21
|
metadata: dict[str, str] | None = None
|
|
22
22
|
skill_path: Path | None = None
|
|
23
23
|
|
|
24
|
+
@property
|
|
25
|
+
def short_description(self) -> str:
|
|
26
|
+
"""Get short description for display in completions.
|
|
27
|
+
|
|
28
|
+
Returns metadata['short-description'] if available, otherwise falls back to description.
|
|
29
|
+
"""
|
|
30
|
+
if self.metadata and "short-description" in self.metadata:
|
|
31
|
+
return self.metadata["short-description"]
|
|
32
|
+
return self.description
|
|
33
|
+
|
|
24
34
|
def to_prompt(self) -> str:
|
|
25
35
|
"""Convert skill to prompt format for agent consumption"""
|
|
26
36
|
return f"""# Skill: {self.name}
|
|
@@ -36,13 +46,15 @@ class Skill:
|
|
|
36
46
|
class SkillLoader:
|
|
37
47
|
"""Load and manage Claude Skills from SKILL.md files"""
|
|
38
48
|
|
|
49
|
+
# System-level skills directory (built-in, lowest priority)
|
|
50
|
+
SYSTEM_SKILLS_DIR: ClassVar[Path] = Path("~/.klaude/skills/.system")
|
|
51
|
+
|
|
39
52
|
# User-level skills directories (checked in order, later ones override earlier ones with same name)
|
|
40
53
|
USER_SKILLS_DIRS: ClassVar[list[Path]] = [
|
|
41
54
|
Path("~/.claude/skills"),
|
|
42
55
|
Path("~/.klaude/skills"),
|
|
43
|
-
# Path("~/.claude/plugins/marketplaces"),
|
|
44
56
|
]
|
|
45
|
-
# Project-level skills directory
|
|
57
|
+
# Project-level skills directory (highest priority)
|
|
46
58
|
PROJECT_SKILLS_DIR: ClassVar[Path] = Path("./.claude/skills")
|
|
47
59
|
|
|
48
60
|
def __init__(self) -> None:
|
|
@@ -54,7 +66,7 @@ class SkillLoader:
|
|
|
54
66
|
|
|
55
67
|
Args:
|
|
56
68
|
skill_path: Path to SKILL.md file
|
|
57
|
-
location: Skill location ('user' or 'project')
|
|
69
|
+
location: Skill location ('system', 'user', or 'project')
|
|
58
70
|
|
|
59
71
|
Returns:
|
|
60
72
|
Skill object or None if loading failed
|
|
@@ -121,39 +133,57 @@ class SkillLoader:
|
|
|
121
133
|
return None
|
|
122
134
|
|
|
123
135
|
def discover_skills(self) -> list[Skill]:
|
|
124
|
-
"""Recursively find all SKILL.md files and load them from
|
|
136
|
+
"""Recursively find all SKILL.md files and load them from system, user and project directories.
|
|
137
|
+
|
|
138
|
+
Loading order (lower priority first, higher priority overrides):
|
|
139
|
+
1. System skills (~/.klaude/skills/.system/) - built-in, lowest priority
|
|
140
|
+
2. User skills (~/.claude/skills/, ~/.klaude/skills/) - user-level
|
|
141
|
+
3. Project skills (./.claude/skills/) - project-level, highest priority
|
|
125
142
|
|
|
126
143
|
Returns:
|
|
127
144
|
List of successfully loaded Skill objects
|
|
128
145
|
"""
|
|
129
146
|
skills: list[Skill] = []
|
|
130
147
|
|
|
131
|
-
# Load
|
|
148
|
+
# Load system-level skills first (lowest priority, can be overridden)
|
|
149
|
+
system_dir = self.SYSTEM_SKILLS_DIR.expanduser()
|
|
150
|
+
if system_dir.exists():
|
|
151
|
+
for skill_file in system_dir.rglob("SKILL.md"):
|
|
152
|
+
skill = self.load_skill(skill_file, location="system")
|
|
153
|
+
if skill:
|
|
154
|
+
skills.append(skill)
|
|
155
|
+
self.loaded_skills[skill.name] = skill
|
|
156
|
+
|
|
157
|
+
# Load user-level skills (override system skills if same name)
|
|
132
158
|
for user_dir in self.USER_SKILLS_DIRS:
|
|
133
159
|
expanded_dir = user_dir.expanduser()
|
|
134
160
|
if expanded_dir.exists():
|
|
135
|
-
for
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
for skill_file in expanded_dir.rglob("SKILL.md"):
|
|
162
|
+
# Skip files under .system directory (already loaded above)
|
|
163
|
+
if ".system" in skill_file.parts:
|
|
164
|
+
continue
|
|
165
|
+
skill = self.load_skill(skill_file, location="user")
|
|
166
|
+
if skill:
|
|
167
|
+
skills.append(skill)
|
|
168
|
+
self.loaded_skills[skill.name] = skill
|
|
141
169
|
|
|
142
170
|
# Load project-level skills (override user skills if same name)
|
|
143
171
|
project_dir = self.PROJECT_SKILLS_DIR.resolve()
|
|
144
172
|
if project_dir.exists():
|
|
145
|
-
for
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
self.loaded_skills[skill.name] = skill
|
|
173
|
+
for skill_file in project_dir.rglob("SKILL.md"):
|
|
174
|
+
skill = self.load_skill(skill_file, location="project")
|
|
175
|
+
if skill:
|
|
176
|
+
skills.append(skill)
|
|
177
|
+
self.loaded_skills[skill.name] = skill
|
|
151
178
|
|
|
152
179
|
# Log discovery summary
|
|
153
180
|
if skills:
|
|
181
|
+
system_count = sum(1 for s in skills if s.location == "system")
|
|
154
182
|
user_count = sum(1 for s in skills if s.location == "user")
|
|
155
183
|
project_count = sum(1 for s in skills if s.location == "project")
|
|
156
184
|
parts: list[str] = []
|
|
185
|
+
if system_count > 0:
|
|
186
|
+
parts.append(f"{system_count} system")
|
|
157
187
|
if user_count > 0:
|
|
158
188
|
parts.append(f"{user_count} user")
|
|
159
189
|
if project_count > 0:
|
|
@@ -171,11 +201,17 @@ class SkillLoader:
|
|
|
171
201
|
Returns:
|
|
172
202
|
Skill object or None if not found
|
|
173
203
|
"""
|
|
204
|
+
# Prefer exact match first (supports namespaced skill names).
|
|
205
|
+
skill = self.loaded_skills.get(name)
|
|
206
|
+
if skill is not None:
|
|
207
|
+
return skill
|
|
208
|
+
|
|
174
209
|
# Support both formats: 'pdf' and 'document-skills:pdf'
|
|
175
210
|
if ":" in name:
|
|
176
|
-
|
|
211
|
+
short = name.split(":")[-1]
|
|
212
|
+
return self.loaded_skills.get(short)
|
|
177
213
|
|
|
178
|
-
return
|
|
214
|
+
return None
|
|
179
215
|
|
|
180
216
|
def list_skills(self) -> list[str]:
|
|
181
217
|
"""Get list of all loaded skill names"""
|
|
@@ -224,25 +260,25 @@ class SkillLoader:
|
|
|
224
260
|
content = re.sub(dir_pattern, replace_dir_path, content)
|
|
225
261
|
|
|
226
262
|
# Pattern 2: Markdown links [text](./path or path)
|
|
227
|
-
# e.g., "[Guide](./docs/guide.md)" -> "[Guide](`/abs/path/to/docs/guide.md`) (use
|
|
263
|
+
# e.g., "[Guide](./docs/guide.md)" -> "[Guide](`/abs/path/to/docs/guide.md`) (use the Read tool to access)"
|
|
228
264
|
link_pattern = r"\[([^\]]+)\]\((\./)?([^\)]+\.md)\)"
|
|
229
265
|
|
|
230
266
|
def replace_link(match: re.Match[str]) -> str:
|
|
231
267
|
text = match.group(1)
|
|
232
268
|
filename = match.group(3)
|
|
233
269
|
abs_path = skill_dir / filename
|
|
234
|
-
return f"[{text}](`{abs_path}`) (use
|
|
270
|
+
return f"[{text}](`{abs_path}`) (use the Read tool to access)"
|
|
235
271
|
|
|
236
272
|
content = re.sub(link_pattern, replace_link, content)
|
|
237
273
|
|
|
238
274
|
# Pattern 3: Standalone markdown references
|
|
239
|
-
# e.g., "see reference.md" -> "see `/abs/path/to/reference.md` (use
|
|
275
|
+
# e.g., "see reference.md" -> "see `/abs/path/to/reference.md` (use the Read tool to access)"
|
|
240
276
|
standalone_pattern = r"(?<!\])\b(\w+\.md)\b(?!\))"
|
|
241
277
|
|
|
242
278
|
def replace_standalone(match: re.Match[str]) -> str:
|
|
243
279
|
filename = match.group(1)
|
|
244
280
|
abs_path = skill_dir / filename
|
|
245
|
-
return f"`{abs_path}` (use
|
|
281
|
+
return f"`{abs_path}` (use the Read tool to access)"
|
|
246
282
|
|
|
247
283
|
content = re.sub(standalone_pattern, replace_standalone, content)
|
|
248
284
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Global skill manager with lazy initialization.
|
|
2
|
+
|
|
3
|
+
This module provides a centralized interface for accessing skills throughout the application.
|
|
4
|
+
Skills are loaded lazily on first access to avoid unnecessary IO at startup.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from klaude_code.skill.loader import Skill, SkillLoader
|
|
8
|
+
from klaude_code.skill.system_skills import install_system_skills
|
|
9
|
+
|
|
10
|
+
_loader: SkillLoader | None = None
|
|
11
|
+
_initialized: bool = False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _ensure_initialized() -> SkillLoader:
|
|
15
|
+
"""Ensure the skill system is initialized and return the loader."""
|
|
16
|
+
global _loader, _initialized
|
|
17
|
+
if not _initialized:
|
|
18
|
+
install_system_skills()
|
|
19
|
+
_loader = SkillLoader()
|
|
20
|
+
_loader.discover_skills()
|
|
21
|
+
_initialized = True
|
|
22
|
+
assert _loader is not None
|
|
23
|
+
return _loader
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_skill_loader() -> SkillLoader:
|
|
27
|
+
"""Get the global skill loader instance.
|
|
28
|
+
|
|
29
|
+
Lazily initializes the skill system on first call.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The global SkillLoader instance
|
|
33
|
+
"""
|
|
34
|
+
return _ensure_initialized()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_skill(name: str) -> Skill | None:
|
|
38
|
+
"""Get a skill by name.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
name: Skill name (supports both 'skill-name' and 'namespace:skill-name')
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Skill object or None if not found
|
|
45
|
+
"""
|
|
46
|
+
return _ensure_initialized().get_skill(name)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_available_skills() -> list[tuple[str, str, str]]:
|
|
50
|
+
"""Get list of available skills for completion and display.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of (name, short_description, location) tuples.
|
|
54
|
+
Uses metadata['short-description'] if available, otherwise falls back to description.
|
|
55
|
+
Skills are ordered by priority: project > user > system.
|
|
56
|
+
"""
|
|
57
|
+
loader = _ensure_initialized()
|
|
58
|
+
skills = [(s.name, s.short_description, s.location) for s in loader.loaded_skills.values()]
|
|
59
|
+
location_order = {"project": 0, "user": 1, "system": 2}
|
|
60
|
+
skills.sort(key=lambda x: location_order.get(x[2], 3))
|
|
61
|
+
return skills
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def list_skill_names() -> list[str]:
|
|
65
|
+
"""Get list of all loaded skill names.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List of skill names
|
|
69
|
+
"""
|
|
70
|
+
return _ensure_initialized().list_skills()
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""System skills management - install built-in skills to user directory.
|
|
2
|
+
|
|
3
|
+
This module handles extracting bundled skills from the package to ~/.klaude/skills/.system/
|
|
4
|
+
on application startup. It uses a fingerprint mechanism to avoid unnecessary re-extraction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import shutil
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from importlib import resources
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from klaude_code.trace import log_debug
|
|
15
|
+
|
|
16
|
+
# Marker file name for tracking installed skills version
|
|
17
|
+
SYSTEM_SKILLS_MARKER_FILENAME = ".klaude-system-skills.marker"
|
|
18
|
+
|
|
19
|
+
# Salt for fingerprint calculation (increment to force re-extraction)
|
|
20
|
+
SYSTEM_SKILLS_MARKER_SALT = "v1"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_system_skills_dir() -> Path:
|
|
24
|
+
"""Get the system skills installation directory.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Path to ~/.klaude/skills/.system/
|
|
28
|
+
"""
|
|
29
|
+
return Path.home() / ".klaude" / "skills" / ".system"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _calculate_fingerprint(assets_dir: Path) -> str:
|
|
33
|
+
"""Calculate a fingerprint hash for the embedded skills assets.
|
|
34
|
+
|
|
35
|
+
The fingerprint is based on all file paths and their contents.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
assets_dir: Path to the assets directory
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Hex string of the hash
|
|
42
|
+
"""
|
|
43
|
+
hasher = hashlib.sha256()
|
|
44
|
+
hasher.update(SYSTEM_SKILLS_MARKER_SALT.encode())
|
|
45
|
+
|
|
46
|
+
if not assets_dir.exists():
|
|
47
|
+
return hasher.hexdigest()
|
|
48
|
+
|
|
49
|
+
# Sort entries for consistent ordering
|
|
50
|
+
for entry in sorted(assets_dir.rglob("*")):
|
|
51
|
+
if entry.is_file():
|
|
52
|
+
# Hash the relative path
|
|
53
|
+
rel_path = entry.relative_to(assets_dir)
|
|
54
|
+
hasher.update(str(rel_path).encode())
|
|
55
|
+
# Hash the file contents
|
|
56
|
+
hasher.update(entry.read_bytes())
|
|
57
|
+
|
|
58
|
+
return hasher.hexdigest()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_marker(marker_path: Path) -> str | None:
|
|
62
|
+
"""Read the fingerprint from the marker file.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
marker_path: Path to the marker file
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The stored fingerprint, or None if the file doesn't exist or is invalid
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
if marker_path.exists():
|
|
72
|
+
return marker_path.read_text(encoding="utf-8").strip()
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _write_marker(marker_path: Path, fingerprint: str) -> None:
|
|
79
|
+
"""Write the fingerprint to the marker file.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
marker_path: Path to the marker file
|
|
83
|
+
fingerprint: The fingerprint to store
|
|
84
|
+
"""
|
|
85
|
+
marker_path.write_text(f"{fingerprint}\n", encoding="utf-8")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@contextmanager
|
|
89
|
+
def _with_embedded_assets_dir() -> Iterator[Path | None]:
|
|
90
|
+
"""Resolve the embedded assets directory as a real filesystem path.
|
|
91
|
+
|
|
92
|
+
Uses `importlib.resources.as_file()` so it works for both normal installs
|
|
93
|
+
and zipimport-style environments.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
assets_ref = resources.files("klaude_code.skill").joinpath("assets")
|
|
97
|
+
with resources.as_file(assets_ref) as assets_path:
|
|
98
|
+
p = Path(assets_path)
|
|
99
|
+
yield p if p.exists() else None
|
|
100
|
+
return
|
|
101
|
+
except (TypeError, AttributeError, ImportError, FileNotFoundError, OSError):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
module_dir = Path(__file__).parent
|
|
106
|
+
assets_path = module_dir / "assets"
|
|
107
|
+
yield assets_path if assets_path.exists() else None
|
|
108
|
+
except (TypeError, NameError, OSError):
|
|
109
|
+
yield None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def install_system_skills() -> bool:
|
|
113
|
+
"""Install system skills from the embedded assets to the user directory.
|
|
114
|
+
|
|
115
|
+
This function:
|
|
116
|
+
1. Calculates a fingerprint of the embedded assets
|
|
117
|
+
2. Checks if the installed skills match (via marker file)
|
|
118
|
+
3. If they don't match, clears and re-extracts the skills
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if skills were installed/updated, False if already up-to-date
|
|
122
|
+
"""
|
|
123
|
+
dest_dir = get_system_skills_dir()
|
|
124
|
+
marker_path = dest_dir / SYSTEM_SKILLS_MARKER_FILENAME
|
|
125
|
+
|
|
126
|
+
with _with_embedded_assets_dir() as assets_path:
|
|
127
|
+
if assets_path is None or not assets_path.exists():
|
|
128
|
+
log_debug("No embedded system skills found")
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Calculate fingerprint of embedded assets
|
|
132
|
+
expected_fingerprint = _calculate_fingerprint(assets_path)
|
|
133
|
+
|
|
134
|
+
# Check if already installed with matching fingerprint
|
|
135
|
+
current_fingerprint = _read_marker(marker_path)
|
|
136
|
+
if current_fingerprint == expected_fingerprint and dest_dir.exists():
|
|
137
|
+
log_debug("System skills already up-to-date")
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
log_debug(f"Installing system skills to {dest_dir}")
|
|
141
|
+
|
|
142
|
+
# Clear existing installation
|
|
143
|
+
if dest_dir.exists():
|
|
144
|
+
try:
|
|
145
|
+
shutil.rmtree(dest_dir)
|
|
146
|
+
except OSError as e:
|
|
147
|
+
log_debug(f"Failed to clear existing system skills: {e}")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
# Create destination directory
|
|
151
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
|
|
153
|
+
# Copy all skill directories from assets
|
|
154
|
+
try:
|
|
155
|
+
for item in assets_path.iterdir():
|
|
156
|
+
if item.is_dir() and not item.name.startswith("."):
|
|
157
|
+
dest_skill_dir = dest_dir / item.name
|
|
158
|
+
shutil.copytree(item, dest_skill_dir)
|
|
159
|
+
log_debug(f"Installed system skill: {item.name}")
|
|
160
|
+
except OSError as e:
|
|
161
|
+
log_debug(f"Failed to copy system skills: {e}")
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
# Write marker file
|
|
165
|
+
try:
|
|
166
|
+
_write_marker(marker_path, expected_fingerprint)
|
|
167
|
+
except OSError as e:
|
|
168
|
+
log_debug(f"Failed to write marker file: {e}")
|
|
169
|
+
# Installation succeeded, just marker failed
|
|
170
|
+
|
|
171
|
+
log_debug("System skills installation complete")
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_installed_system_skills() -> list[Path]:
|
|
176
|
+
"""Get list of installed system skill directories.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of paths to installed skill directories
|
|
180
|
+
"""
|
|
181
|
+
dest_dir = get_system_skills_dir()
|
|
182
|
+
if not dest_dir.exists():
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
skills: list[Path] = []
|
|
186
|
+
for item in dest_dir.iterdir():
|
|
187
|
+
if item.is_dir() and not item.name.startswith("."):
|
|
188
|
+
skill_file = item / "SKILL.md"
|
|
189
|
+
if skill_file.exists():
|
|
190
|
+
skills.append(item)
|
|
191
|
+
|
|
192
|
+
return skills
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
"""REPL completion handlers for @ file paths
|
|
1
|
+
"""REPL completion handlers for @ file paths, / slash commands, and $ skills.
|
|
2
2
|
|
|
3
3
|
This module provides completers for the REPL input:
|
|
4
4
|
- _SlashCommandCompleter: Completes slash commands on the first line
|
|
5
|
+
- _SkillCompleter: Completes skill names on the first line with $ prefix
|
|
5
6
|
- _AtFilesCompleter: Completes @path segments using fd or ripgrep
|
|
6
|
-
- _ComboCompleter: Combines
|
|
7
|
+
- _ComboCompleter: Combines all completers with priority logic
|
|
7
8
|
|
|
8
9
|
Public API:
|
|
9
10
|
- create_repl_completer(): Factory function to create the combined completer
|
|
10
11
|
- AT_TOKEN_PATTERN: Regex pattern for @token matching (used by key bindings)
|
|
12
|
+
- SKILL_TOKEN_PATTERN: Regex pattern for $skill matching (used by key bindings)
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
@@ -34,6 +36,9 @@ from klaude_code.trace.log import DebugType, log_debug
|
|
|
34
36
|
# single logical token.
|
|
35
37
|
AT_TOKEN_PATTERN = re.compile(r'(^|\s)@(?P<frag>"[^"]*"|[^\s]*)$')
|
|
36
38
|
|
|
39
|
+
# Pattern to match $skill or ¥skill token for skill completion (used by key bindings).
|
|
40
|
+
SKILL_TOKEN_PATTERN = re.compile(r"^[$¥](?P<frag>\S*)$")
|
|
41
|
+
|
|
37
42
|
|
|
38
43
|
def create_repl_completer() -> Completer:
|
|
39
44
|
"""Create and return the combined REPL completer.
|
|
@@ -121,12 +126,102 @@ class _SlashCommandCompleter(Completer):
|
|
|
121
126
|
return bool(self._SLASH_TOKEN_RE.search(text_before))
|
|
122
127
|
|
|
123
128
|
|
|
129
|
+
class _SkillCompleter(Completer):
|
|
130
|
+
"""Complete skill names at the beginning of the first line.
|
|
131
|
+
|
|
132
|
+
Behavior:
|
|
133
|
+
- Only triggers when cursor is on first line and text matches $ or ¥...
|
|
134
|
+
- Shows available skills with descriptions
|
|
135
|
+
- Inserts trailing space after completion
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
_SKILL_TOKEN_RE = SKILL_TOKEN_PATTERN
|
|
139
|
+
|
|
140
|
+
def get_completions(
|
|
141
|
+
self,
|
|
142
|
+
document: Document,
|
|
143
|
+
complete_event, # type: ignore[override]
|
|
144
|
+
) -> Iterable[Completion]:
|
|
145
|
+
# Only complete on first line
|
|
146
|
+
if document.cursor_position_row != 0:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
text_before = document.current_line_before_cursor
|
|
150
|
+
m = self._SKILL_TOKEN_RE.search(text_before)
|
|
151
|
+
if not m:
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
frag = m.group("frag").lower()
|
|
155
|
+
# Get the prefix character ($ or ¥)
|
|
156
|
+
prefix_char = text_before[0]
|
|
157
|
+
token_start = len(text_before) - len(f"{prefix_char}{m.group('frag')}")
|
|
158
|
+
start_position = token_start - len(text_before) # negative offset
|
|
159
|
+
|
|
160
|
+
# Get available skills from SkillTool
|
|
161
|
+
skills = self._get_available_skills()
|
|
162
|
+
if not skills:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# Filter skills that match the fragment (case-insensitive)
|
|
166
|
+
matched: list[tuple[str, str, str]] = [] # (name, description, location)
|
|
167
|
+
for name, desc, location in skills:
|
|
168
|
+
if frag in name.lower() or frag in desc.lower():
|
|
169
|
+
matched.append((name, desc, location))
|
|
170
|
+
|
|
171
|
+
if not matched:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# Calculate max width for alignment
|
|
175
|
+
max_name_len = max(len(name) for name, _, _ in matched)
|
|
176
|
+
align_width = max(max_name_len, 20) + 2
|
|
177
|
+
|
|
178
|
+
for name, desc, location in matched:
|
|
179
|
+
# Format: name [location] description
|
|
180
|
+
# Align location tags (max length is "project" = 7, plus brackets = 9)
|
|
181
|
+
padding_name = " " * (align_width - len(name))
|
|
182
|
+
location_tag = f"[{location}]".ljust(9)
|
|
183
|
+
|
|
184
|
+
# Using HTML for formatting: bold skill name, cyan location tag, gray description
|
|
185
|
+
display_text = HTML(
|
|
186
|
+
f"<b>{name}</b>{padding_name}<style color='ansicyan'>{location_tag}</style> "
|
|
187
|
+
f"<style color='ansibrightblack'>{desc}</style>"
|
|
188
|
+
)
|
|
189
|
+
completion_text = f"${name} "
|
|
190
|
+
yield Completion(
|
|
191
|
+
text=completion_text,
|
|
192
|
+
start_position=start_position,
|
|
193
|
+
display=display_text,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _get_available_skills(self) -> list[tuple[str, str, str]]:
|
|
197
|
+
"""Get available skills from skill module.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of (name, description, location) tuples
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
# Import here to avoid circular imports
|
|
204
|
+
from klaude_code.skill import get_available_skills
|
|
205
|
+
|
|
206
|
+
return get_available_skills()
|
|
207
|
+
except Exception:
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
def is_skill_context(self, document: Document) -> bool:
|
|
211
|
+
"""Check if current context is a skill completion."""
|
|
212
|
+
if document.cursor_position_row != 0:
|
|
213
|
+
return False
|
|
214
|
+
text_before = document.current_line_before_cursor
|
|
215
|
+
return bool(self._SKILL_TOKEN_RE.search(text_before))
|
|
216
|
+
|
|
217
|
+
|
|
124
218
|
class _ComboCompleter(Completer):
|
|
125
|
-
"""Combined completer that handles
|
|
219
|
+
"""Combined completer that handles @ file paths, / slash commands, and $ skills."""
|
|
126
220
|
|
|
127
221
|
def __init__(self) -> None:
|
|
128
222
|
self._at_completer = _AtFilesCompleter()
|
|
129
223
|
self._slash_completer = _SlashCommandCompleter()
|
|
224
|
+
self._skill_completer = _SkillCompleter()
|
|
130
225
|
|
|
131
226
|
def get_completions(
|
|
132
227
|
self,
|
|
@@ -138,6 +233,11 @@ class _ComboCompleter(Completer):
|
|
|
138
233
|
yield from self._slash_completer.get_completions(document, complete_event)
|
|
139
234
|
return
|
|
140
235
|
|
|
236
|
+
# Try skill completion (only on first line with $ prefix)
|
|
237
|
+
if document.cursor_position_row == 0 and self._skill_completer.is_skill_context(document):
|
|
238
|
+
yield from self._skill_completer.get_completions(document, complete_event)
|
|
239
|
+
return
|
|
240
|
+
|
|
141
241
|
# Fall back to @ file completion
|
|
142
242
|
yield from self._at_completer.get_completions(document, complete_event)
|
|
143
243
|
|
|
@@ -2,12 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
|
+
from rich.rule import Rule
|
|
5
6
|
from rich.text import Text
|
|
6
7
|
|
|
7
8
|
from klaude_code import const
|
|
8
9
|
from klaude_code.protocol import events
|
|
9
10
|
from klaude_code.ui.core.stage_manager import Stage, StageManager
|
|
10
11
|
from klaude_code.ui.modes.repl.renderer import REPLRenderer
|
|
12
|
+
from klaude_code.ui.renderers.assistant import ASSISTANT_MESSAGE_MARK
|
|
11
13
|
from klaude_code.ui.renderers.thinking import normalize_thinking_content
|
|
12
14
|
from klaude_code.ui.rich.markdown import MarkdownStream, ThinkingMarkdown
|
|
13
15
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
@@ -327,7 +329,7 @@ class DisplayEventHandler:
|
|
|
327
329
|
theme=self.renderer.themes.thinking_markdown_theme,
|
|
328
330
|
console=self.renderer.console,
|
|
329
331
|
spinner=self.renderer.spinner_renderable(),
|
|
330
|
-
|
|
332
|
+
left_margin=const.MARKDOWN_LEFT_MARGIN,
|
|
331
333
|
markdown_class=ThinkingMarkdown,
|
|
332
334
|
)
|
|
333
335
|
self.thinking_stream.start(mdstream)
|
|
@@ -358,8 +360,8 @@ class DisplayEventHandler:
|
|
|
358
360
|
theme=self.renderer.themes.markdown_theme,
|
|
359
361
|
console=self.renderer.console,
|
|
360
362
|
spinner=self.renderer.spinner_renderable(),
|
|
361
|
-
mark=
|
|
362
|
-
|
|
363
|
+
mark=ASSISTANT_MESSAGE_MARK,
|
|
364
|
+
left_margin=const.MARKDOWN_LEFT_MARGIN,
|
|
363
365
|
)
|
|
364
366
|
self.assistant_stream.start(mdstream)
|
|
365
367
|
self.assistant_stream.append(event.content)
|
|
@@ -430,6 +432,8 @@ class DisplayEventHandler:
|
|
|
430
432
|
emit_osc94(OSC94States.HIDDEN)
|
|
431
433
|
self.spinner_status.reset()
|
|
432
434
|
self.renderer.spinner_stop()
|
|
435
|
+
self.renderer.console.print(Rule(characters="-", style=ThemeKey.LINES))
|
|
436
|
+
self.renderer.print()
|
|
433
437
|
await self.stage_manager.transition_to(Stage.WAITING)
|
|
434
438
|
self._maybe_notify_task_finish(event)
|
|
435
439
|
|