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.
Files changed (44) hide show
  1. klaude_code/command/status_cmd.py +1 -1
  2. klaude_code/const/__init__.py +8 -2
  3. klaude_code/core/manager/sub_agent_manager.py +1 -1
  4. klaude_code/core/reminders.py +51 -0
  5. klaude_code/core/task.py +37 -18
  6. klaude_code/core/tool/__init__.py +1 -4
  7. klaude_code/core/tool/skill/__init__.py +0 -0
  8. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -39
  9. klaude_code/protocol/model.py +2 -1
  10. klaude_code/session/export.py +1 -1
  11. klaude_code/session/store.py +4 -2
  12. klaude_code/skill/__init__.py +27 -0
  13. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  14. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  15. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  16. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  17. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  18. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +60 -24
  19. klaude_code/skill/manager.py +70 -0
  20. klaude_code/skill/system_skills.py +192 -0
  21. klaude_code/ui/modes/repl/completers.py +103 -3
  22. klaude_code/ui/modes/repl/event_handler.py +7 -3
  23. klaude_code/ui/modes/repl/input_prompt_toolkit.py +42 -3
  24. klaude_code/ui/renderers/assistant.py +7 -2
  25. klaude_code/ui/renderers/developer.py +12 -0
  26. klaude_code/ui/renderers/diffs.py +1 -1
  27. klaude_code/ui/renderers/metadata.py +4 -2
  28. klaude_code/ui/renderers/thinking.py +1 -1
  29. klaude_code/ui/renderers/tools.py +57 -32
  30. klaude_code/ui/renderers/user_input.py +32 -2
  31. klaude_code/ui/rich/markdown.py +22 -17
  32. klaude_code/ui/rich/status.py +1 -13
  33. klaude_code/ui/rich/theme.py +7 -5
  34. {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/METADATA +18 -13
  35. {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/RECORD +38 -35
  36. klaude_code/command/prompt-deslop.md +0 -14
  37. klaude_code/command/prompt-dev-docs-update.md +0 -56
  38. klaude_code/command/prompt-dev-docs.md +0 -46
  39. klaude_code/command/prompt-handoff.md +0 -33
  40. klaude_code/command/prompt-jj-workspace.md +0 -18
  41. klaude_code/core/tool/memory/__init__.py +0 -5
  42. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  43. {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/WHEEL +0 -0
  44. {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 both user and project directories
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 user-level skills from all directories
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 pattern in ("SKILL.md", "skill.md"):
136
- for skill_file in expanded_dir.rglob(pattern):
137
- skill = self.load_skill(skill_file, location="user")
138
- if skill:
139
- skills.append(skill)
140
- self.loaded_skills[skill.name] = skill
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 pattern in ("SKILL.md", "skill.md"):
146
- for skill_file in project_dir.rglob(pattern):
147
- skill = self.load_skill(skill_file, location="project")
148
- if skill:
149
- skills.append(skill)
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
- name = name.split(":")[-1]
211
+ short = name.split(":")[-1]
212
+ return self.loaded_skills.get(short)
177
213
 
178
- return self.loaded_skills.get(name)
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 read_file to access)"
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 read_file to access)"
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 read_file to access)"
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 read_file to access)"
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