klaude-code 1.2.22__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/command/status_cmd.py +1 -1
- klaude_code/const/__init__.py +8 -2
- klaude_code/core/manager/sub_agent_manager.py +1 -1
- klaude_code/core/reminders.py +51 -0
- klaude_code/core/task.py +37 -18
- klaude_code/core/tool/__init__.py +1 -4
- klaude_code/core/tool/skill/__init__.py +0 -0
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -39
- klaude_code/protocol/model.py +2 -1
- klaude_code/session/export.py +1 -1
- klaude_code/session/store.py +4 -2
- 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/developer.py +12 -0
- klaude_code/ui/renderers/diffs.py +1 -1
- klaude_code/ui/renderers/metadata.py +4 -2
- klaude_code/ui/renderers/thinking.py +1 -1
- klaude_code/ui/renderers/tools.py +57 -32
- klaude_code/ui/renderers/user_input.py +32 -2
- klaude_code/ui/rich/markdown.py +22 -17
- klaude_code/ui/rich/status.py +1 -13
- klaude_code/ui/rich/theme.py +7 -5
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/METADATA +18 -13
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/RECORD +38 -35
- 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/memory/__init__.py +0 -5
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-creator
|
|
3
|
+
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
|
|
4
|
+
metadata:
|
|
5
|
+
short-description: Create or update a skill
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Skill Creator
|
|
9
|
+
|
|
10
|
+
This skill provides guidance for creating effective skills.
|
|
11
|
+
|
|
12
|
+
## About Skills
|
|
13
|
+
|
|
14
|
+
Skills are modular, self-contained packages that extend the agent's capabilities by providing
|
|
15
|
+
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
|
|
16
|
+
domains or tasks - they transform the agent from a general-purpose assistant into a specialized
|
|
17
|
+
agent equipped with procedural knowledge.
|
|
18
|
+
|
|
19
|
+
### What Skills Provide
|
|
20
|
+
|
|
21
|
+
1. Specialized workflows - Multi-step procedures for specific domains
|
|
22
|
+
2. Tool integrations - Instructions for working with specific file formats or APIs
|
|
23
|
+
3. Domain expertise - Company-specific knowledge, schemas, business logic
|
|
24
|
+
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
|
|
25
|
+
|
|
26
|
+
## Core Principles
|
|
27
|
+
|
|
28
|
+
### Concise is Key
|
|
29
|
+
|
|
30
|
+
The context window is a public good. Skills share the context window with everything else:
|
|
31
|
+
system prompt, conversation history, other Skills' metadata, and the actual user request.
|
|
32
|
+
|
|
33
|
+
**Default assumption: The agent is already very smart.** Only add context the agent doesn't
|
|
34
|
+
already have. Challenge each piece of information: "Does the agent really need this explanation?"
|
|
35
|
+
and "Does this paragraph justify its token cost?"
|
|
36
|
+
|
|
37
|
+
Prefer concise examples over verbose explanations.
|
|
38
|
+
|
|
39
|
+
### Anatomy of a Skill
|
|
40
|
+
|
|
41
|
+
Every skill consists of a required SKILL.md file and optional bundled resources:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
skill-name/
|
|
45
|
+
├── SKILL.md (required)
|
|
46
|
+
│ ├── YAML frontmatter metadata (required)
|
|
47
|
+
│ │ ├── name: (required)
|
|
48
|
+
│ │ └── description: (required)
|
|
49
|
+
│ └── Markdown instructions (required)
|
|
50
|
+
└── Bundled Resources (optional)
|
|
51
|
+
├── scripts/ - Executable code (Python/Bash/etc.)
|
|
52
|
+
├── references/ - Documentation intended to be loaded into context as needed
|
|
53
|
+
└── assets/ - Files used in output (templates, icons, fonts, etc.)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### SKILL.md (required)
|
|
57
|
+
|
|
58
|
+
Every SKILL.md consists of:
|
|
59
|
+
|
|
60
|
+
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields
|
|
61
|
+
that determine when the skill gets used, thus it is very important to be clear and comprehensive
|
|
62
|
+
in describing what the skill is, and when it should be used.
|
|
63
|
+
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the
|
|
64
|
+
skill triggers (if at all).
|
|
65
|
+
|
|
66
|
+
#### Bundled Resources (optional)
|
|
67
|
+
|
|
68
|
+
##### Scripts (`scripts/`)
|
|
69
|
+
|
|
70
|
+
Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are
|
|
71
|
+
repeatedly rewritten.
|
|
72
|
+
|
|
73
|
+
- **When to include**: When the same code is being rewritten repeatedly or deterministic
|
|
74
|
+
reliability is needed
|
|
75
|
+
- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks
|
|
76
|
+
- **Benefits**: Token efficient, deterministic, may be executed without loading into context
|
|
77
|
+
|
|
78
|
+
##### References (`references/`)
|
|
79
|
+
|
|
80
|
+
Documentation and reference material intended to be loaded as needed into context.
|
|
81
|
+
|
|
82
|
+
- **When to include**: For documentation that the agent should reference while working
|
|
83
|
+
- **Examples**: `references/schema.md` for database schemas, `references/api_docs.md` for
|
|
84
|
+
API specifications
|
|
85
|
+
- **Benefits**: Keeps SKILL.md lean, loaded only when needed
|
|
86
|
+
|
|
87
|
+
##### Assets (`assets/`)
|
|
88
|
+
|
|
89
|
+
Files not intended to be loaded into context, but rather used within the output.
|
|
90
|
+
|
|
91
|
+
- **When to include**: When the skill needs files that will be used in the final output
|
|
92
|
+
- **Examples**: `assets/logo.png` for brand assets, `assets/template.html` for HTML templates
|
|
93
|
+
- **Benefits**: Separates output resources from documentation
|
|
94
|
+
|
|
95
|
+
## Skill Creation Process
|
|
96
|
+
|
|
97
|
+
Skill creation involves these steps:
|
|
98
|
+
|
|
99
|
+
1. Understand the skill with concrete examples
|
|
100
|
+
2. Plan reusable skill contents (scripts, references, assets)
|
|
101
|
+
3. Create the skill directory structure
|
|
102
|
+
4. Write SKILL.md with proper frontmatter
|
|
103
|
+
5. Add bundled resources as needed
|
|
104
|
+
6. Test and iterate based on real usage
|
|
105
|
+
|
|
106
|
+
### Skill Naming
|
|
107
|
+
|
|
108
|
+
- Use lowercase letters, digits, and hyphens only
|
|
109
|
+
- Prefer short, verb-led phrases that describe the action
|
|
110
|
+
- Name the skill folder exactly after the skill name
|
|
111
|
+
|
|
112
|
+
### Writing Guidelines
|
|
113
|
+
|
|
114
|
+
Always use imperative/infinitive form.
|
|
115
|
+
|
|
116
|
+
#### Frontmatter
|
|
117
|
+
|
|
118
|
+
Write the YAML frontmatter with `name` and `description`:
|
|
119
|
+
|
|
120
|
+
- `name`: The skill name (required)
|
|
121
|
+
- `description`: This is the primary triggering mechanism for your skill. Include both what
|
|
122
|
+
the Skill does and specific triggers/contexts for when to use it. Include all "when to use"
|
|
123
|
+
information here - Not in the body.
|
|
124
|
+
|
|
125
|
+
#### Body
|
|
126
|
+
|
|
127
|
+
Write instructions for using the skill and its bundled resources. Keep SKILL.md body to the
|
|
128
|
+
essentials and under 500 lines to minimize context bloat.
|
|
129
|
+
|
|
130
|
+
## Skill Storage Locations
|
|
131
|
+
|
|
132
|
+
Skills can be stored in multiple locations with the following priority (higher priority overrides lower):
|
|
133
|
+
|
|
134
|
+
| Priority | Scope | Path | Description |
|
|
135
|
+
|----------|---------|-----------------------------|-----------------------|
|
|
136
|
+
| 1 | Project | `.claude/skills/` | Current project only |
|
|
137
|
+
| 2 | User | `~/.klaude/skills/` | User-level |
|
|
138
|
+
| 3 | User | `~/.claude/skills/` | User-level (Claude) |
|
|
139
|
+
| 4 | System | `~/.klaude/skills/.system/` | Built-in system skills|
|
|
@@ -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
|