zjcode 0.0.1__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 (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. zjcode-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,1252 @@
1
+ """CLI commands for skill management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any, assert_never
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+
13
+ from deepagents.middleware.skills import SkillMetadata
14
+
15
+ from deepagents_code.output import OutputFormat
16
+
17
+ from deepagents_code import theme
18
+
19
+ MAX_SKILL_NAME_LENGTH = 64
20
+
21
+
22
+ def _validate_name(name: str) -> tuple[bool, str]:
23
+ """Validate name per Agent Skills spec.
24
+
25
+ Requirements (https://agentskills.io/specification):
26
+ - Max 64 characters
27
+ - Unicode lowercase alphanumeric and hyphens only
28
+ - Cannot start or end with hyphen
29
+ - No consecutive hyphens
30
+ - No path traversal sequences
31
+
32
+ Unicode lowercase alphanumeric means any character where
33
+ `c.isalpha() and c.islower()` or `c.isdigit()` returns `True`,
34
+ which covers accented Latin characters (e.g., `'cafe'`,
35
+ `'uber-tool'`) and other scripts. This matches the SDK's
36
+ `_validate_skill_name` implementation.
37
+
38
+ Args:
39
+ name: The name to validate.
40
+
41
+ Returns:
42
+ Tuple of (is_valid, error_message). If valid, error_message is empty.
43
+ """
44
+ # Check for empty or whitespace-only names
45
+ if not name or not name.strip():
46
+ return False, "cannot be empty"
47
+
48
+ # Check length (spec: max 64 chars)
49
+ if len(name) > MAX_SKILL_NAME_LENGTH:
50
+ return False, "cannot exceed 64 characters"
51
+
52
+ # Check for path traversal sequences (dcode-specific; the SDK validates
53
+ # against the directory name instead, but dcode accepts user input
54
+ # directly so we need explicit path-safety checks)
55
+ if ".." in name or "/" in name or "\\" in name:
56
+ return False, "cannot contain path components"
57
+
58
+ # Structural hyphen checks
59
+ if name.startswith("-") or name.endswith("-") or "--" in name:
60
+ return (
61
+ False,
62
+ "must be lowercase alphanumeric with single hyphens only",
63
+ )
64
+
65
+ # Character-by-character check (matches SDK's _validate_skill_name)
66
+ for c in name:
67
+ if c == "-":
68
+ continue
69
+ if (c.isalpha() and c.islower()) or c.isdigit():
70
+ continue
71
+ return (
72
+ False,
73
+ "must be lowercase alphanumeric with single hyphens only",
74
+ )
75
+
76
+ return True, ""
77
+
78
+
79
+ def _validate_skill_path(skill_dir: Path, base_dir: Path) -> tuple[bool, str]:
80
+ """Validate that the resolved skill directory is within the base directory.
81
+
82
+ Args:
83
+ skill_dir: The skill directory path to validate
84
+ base_dir: The base skills directory that should contain skill_dir
85
+
86
+ Returns:
87
+ Tuple of (is_valid, error_message). If valid, error_message is empty.
88
+ """
89
+ try:
90
+ # Resolve both paths to their canonical form
91
+ resolved_skill = skill_dir.resolve()
92
+ resolved_base = base_dir.resolve()
93
+
94
+ # Check if skill_dir is within base_dir
95
+ if not resolved_skill.is_relative_to(resolved_base):
96
+ return False, f"Skill directory must be within {base_dir}"
97
+ except (OSError, RuntimeError) as e:
98
+ return False, f"Invalid path: {e}"
99
+ else:
100
+ return True, ""
101
+
102
+
103
+ def _format_info_fields(skill: SkillMetadata) -> list[tuple[str, str]]:
104
+ """Extract non-empty optional metadata fields for display.
105
+
106
+ The upstream `_parse_skill_metadata` normalises empty/whitespace license
107
+ and compatibility values to `None`, so the truthy checks below are
108
+ sufficient.
109
+
110
+ Args:
111
+ skill: Skill metadata to extract display fields from.
112
+
113
+ Returns:
114
+ Ordered list of (label, value) tuples for non-empty fields.
115
+ Fields appear in order: License, Compatibility, Allowed Tools,
116
+ Metadata.
117
+ """
118
+ fields: list[tuple[str, str]] = []
119
+ license_val = skill.get("license")
120
+ if license_val:
121
+ fields.append(("License", license_val))
122
+ compat_val = skill.get("compatibility")
123
+ if compat_val:
124
+ fields.append(("Compatibility", compat_val))
125
+ if skill.get("allowed_tools"):
126
+ fields.append(
127
+ ("Allowed Tools", ", ".join(str(t) for t in skill["allowed_tools"]))
128
+ )
129
+ meta = skill.get("metadata")
130
+ if meta and isinstance(meta, dict):
131
+ formatted = ", ".join(f"{k}={v}" for k, v in meta.items())
132
+ fields.append(("Metadata", formatted))
133
+ return fields
134
+
135
+
136
+ def _list(
137
+ agent: str, *, project: bool = False, output_format: OutputFormat = "text"
138
+ ) -> None:
139
+ """List all available skills for the specified agent.
140
+
141
+ Args:
142
+ agent: Agent identifier for skills (default: agent).
143
+ project: If True, show only project skills.
144
+ If False, show all skills (user + project).
145
+ output_format: Output format — `'text'` (Rich) or `'json'`.
146
+ """
147
+ from rich.markup import escape as escape_markup
148
+
149
+ from deepagents_code.config import Settings, console, get_glyphs
150
+ from deepagents_code.skills.load import list_skills
151
+
152
+ settings = Settings.from_environment()
153
+ user_skills_dir = settings.get_user_skills_dir(agent)
154
+ project_skills_dir = settings.get_project_skills_dir()
155
+ user_agent_skills_dir = settings.get_user_agent_skills_dir()
156
+ project_agent_skills_dir = settings.get_project_agent_skills_dir()
157
+
158
+ # If --project flag is used, only show project skills
159
+ if project:
160
+ if not project_skills_dir:
161
+ if output_format == "json":
162
+ from deepagents_code.output import write_json
163
+
164
+ write_json("skills list", [])
165
+ return
166
+ console.print("[yellow]Not in a project directory.[/yellow]")
167
+ console.print(
168
+ "[dim]Project skills require a .git directory "
169
+ "in the project root.[/dim]",
170
+ style=theme.MUTED,
171
+ )
172
+ return
173
+
174
+ # Check both project skill directories
175
+ has_deepagents_skills = project_skills_dir.exists() and any(
176
+ project_skills_dir.iterdir()
177
+ )
178
+ has_agent_skills = (
179
+ project_agent_skills_dir
180
+ and project_agent_skills_dir.exists()
181
+ and any(project_agent_skills_dir.iterdir())
182
+ )
183
+
184
+ if not has_deepagents_skills and not has_agent_skills:
185
+ if output_format == "json":
186
+ from deepagents_code.output import write_json
187
+
188
+ write_json("skills list", [])
189
+ return
190
+ console.print("[yellow]No project skills found.[/yellow]")
191
+ console.print(
192
+ f"[dim]Project skills will be created in {project_skills_dir}/ "
193
+ "when you add them.[/dim]",
194
+ style=theme.MUTED,
195
+ )
196
+ console.print(
197
+ "\n[dim]Create a project skill:\n"
198
+ " dcode skills create my-skill --project[/dim]",
199
+ style=theme.MUTED,
200
+ )
201
+ return
202
+
203
+ skills = list_skills(
204
+ user_skills_dir=None,
205
+ project_skills_dir=project_skills_dir,
206
+ user_agent_skills_dir=None,
207
+ project_agent_skills_dir=project_agent_skills_dir,
208
+ )
209
+
210
+ if output_format == "json":
211
+ from deepagents_code.output import write_json
212
+
213
+ write_json("skills list", [dict(s) for s in skills])
214
+ return
215
+
216
+ console.print("\n[bold]Project Skills:[/bold]\n", style=theme.PRIMARY)
217
+ else:
218
+ # Load skills from all directories (including built-in)
219
+ skills = list_skills(
220
+ built_in_skills_dir=settings.get_built_in_skills_dir(),
221
+ user_skills_dir=user_skills_dir,
222
+ project_skills_dir=project_skills_dir,
223
+ user_agent_skills_dir=user_agent_skills_dir,
224
+ project_agent_skills_dir=project_agent_skills_dir,
225
+ )
226
+
227
+ if output_format == "json":
228
+ from deepagents_code.output import write_json
229
+
230
+ write_json("skills list", [dict(s) for s in skills])
231
+ return
232
+
233
+ if not skills:
234
+ console.print()
235
+ console.print("[yellow]No skills found.[/yellow]")
236
+ console.print()
237
+ console.print(
238
+ "[dim]Skills are loaded from these directories "
239
+ "(highest precedence first):\n"
240
+ " 1. .agents/skills/ project skills\n"
241
+ " 2. .deepagents/skills/ project skills (alias)\n"
242
+ " 3. ~/.agents/skills/ user skills\n"
243
+ " 4. ~/.deepagents/<agent>/skills/ user skills (alias)\n"
244
+ " 5. <package>/built_in_skills/ built-in skills[/dim]",
245
+ style=theme.MUTED,
246
+ )
247
+ console.print(
248
+ "\n[dim]Create your first skill:\n dcode skills create my-skill[/dim]",
249
+ style=theme.MUTED,
250
+ )
251
+ return
252
+
253
+ console.print("\n[bold]Available Skills:[/bold]\n", style=theme.PRIMARY)
254
+
255
+ # Group skills by source
256
+ user_skills = [s for s in skills if s["source"] == "user"]
257
+ project_skills_list = [s for s in skills if s["source"] == "project"]
258
+ built_in_skills_list = [s for s in skills if s["source"] == "built-in"]
259
+
260
+ # Show user skills
261
+ if user_skills and not project:
262
+ console.print("[bold cyan]User Skills:[/bold cyan]", style=theme.PRIMARY)
263
+ bullet = get_glyphs().bullet
264
+ for skill in user_skills:
265
+ skill_path = Path(skill["path"])
266
+ name = escape_markup(skill["name"])
267
+ console.print(f" {bullet} [bold]{name}[/bold]", style=theme.PRIMARY)
268
+ console.print(
269
+ f" {escape_markup(str(skill_path.parent))}/",
270
+ style=theme.MUTED,
271
+ )
272
+ console.print()
273
+ console.print(
274
+ f" {escape_markup(skill['description'])}",
275
+ style=theme.MUTED,
276
+ )
277
+ console.print()
278
+
279
+ # Show project skills
280
+ if project_skills_list:
281
+ if not project and user_skills:
282
+ console.print()
283
+ console.print("[bold green]Project Skills:[/bold green]", style=theme.PRIMARY)
284
+ bullet = get_glyphs().bullet
285
+ for skill in project_skills_list:
286
+ skill_path = Path(skill["path"])
287
+ name = escape_markup(skill["name"])
288
+ console.print(f" {bullet} [bold]{name}[/bold]", style=theme.PRIMARY)
289
+ console.print(
290
+ f" {escape_markup(str(skill_path.parent))}/",
291
+ style=theme.MUTED,
292
+ )
293
+ console.print()
294
+ console.print(
295
+ f" {escape_markup(skill['description'])}",
296
+ style=theme.MUTED,
297
+ )
298
+ console.print()
299
+
300
+ # Show built-in skills
301
+ if built_in_skills_list and not project:
302
+ if user_skills or project_skills_list:
303
+ console.print()
304
+ console.print(
305
+ "[bold magenta]Built-in Skills:[/bold magenta]", style=theme.PRIMARY
306
+ )
307
+ bullet = get_glyphs().bullet
308
+ for skill in built_in_skills_list:
309
+ name = escape_markup(skill["name"])
310
+ console.print(f" {bullet} [bold]{name}[/bold]", style=theme.PRIMARY)
311
+ console.print()
312
+ console.print(
313
+ f" {escape_markup(skill['description'])}",
314
+ style=theme.MUTED,
315
+ )
316
+ console.print()
317
+
318
+
319
+ def _generate_template(skill_name: str) -> str:
320
+ """Generate a `SKILL.md` template for a new skill.
321
+
322
+ The template follows the Agent Skills spec
323
+ (https://agentskills.io/specification) and the skill-creator guidance:
324
+
325
+ - Description includes "when to use" trigger information (not the body)
326
+ - Body contains only instructions loaded after the skill triggers
327
+
328
+ Args:
329
+ skill_name: Name of the skill (used in frontmatter and heading).
330
+
331
+ Returns:
332
+ Complete `SKILL.md` content with YAML frontmatter and markdown body.
333
+ """
334
+ title = skill_name.title().replace("-", " ")
335
+ description = (
336
+ "TODO: Explain what this skill does and when to use it. "
337
+ "Include specific triggers — scenarios, file types, or phrases "
338
+ "that should activate this skill. Example: 'Create and edit PDF "
339
+ "documents. Use when the user asks to merge, split, fill, or "
340
+ "annotate PDF files.'"
341
+ )
342
+ return f"""---
343
+ name: {skill_name}
344
+ description: "{description}"
345
+ # (Warning: SKILL.md files exceeding 10 MB are silently skipped at load time.)
346
+ # Optional fields per Agent Skills spec:
347
+ # license: Apache-2.0
348
+ # compatibility: Designed for Deep Agents Code
349
+ # metadata:
350
+ # author: your-org
351
+ # version: "1.0"
352
+ # allowed-tools: Bash(git:*) Read
353
+ ---
354
+
355
+ # {title}
356
+
357
+ ## Overview
358
+
359
+ [TODO: 1-2 sentences explaining what this skill enables]
360
+
361
+ ## Instructions
362
+
363
+ ### Step 1: [First Action]
364
+ [Explain what to do first]
365
+
366
+ ### Step 2: [Second Action]
367
+ [Explain what to do next]
368
+
369
+ ### Step 3: [Final Action]
370
+ [Explain how to complete the task]
371
+
372
+ ## Best Practices
373
+
374
+ - [Best practice 1]
375
+ - [Best practice 2]
376
+ - [Best practice 3]
377
+
378
+ ## Examples
379
+
380
+ ### Example 1: [Scenario Name]
381
+
382
+ **User Request:** "[Example user request]"
383
+
384
+ **Approach:**
385
+ 1. [Step-by-step breakdown]
386
+ 2. [Using tools and commands]
387
+ 3. [Expected outcome]
388
+ """
389
+
390
+
391
+ def _create(
392
+ skill_name: str,
393
+ agent: str,
394
+ project: bool = False,
395
+ *,
396
+ output_format: OutputFormat = "text",
397
+ ) -> None:
398
+ """Create a new skill with a template SKILL.md file.
399
+
400
+ Args:
401
+ skill_name: Name of the skill to create.
402
+ agent: Agent identifier for skills
403
+ project: If True, create in project skills directory.
404
+ If False, create in user skills directory.
405
+ output_format: Output format — `'text'` (Rich) or `'json'`.
406
+
407
+ Raises:
408
+ SystemExit: If the skill name is invalid or the directory cannot be created.
409
+ """
410
+ from deepagents_code.config import Settings, console, get_glyphs
411
+
412
+ # Validate skill name first (per Agent Skills spec)
413
+ is_valid, error_msg = _validate_name(skill_name)
414
+ if not is_valid:
415
+ console.print(f"[bold red]Error:[/bold red] Invalid skill name: {error_msg}")
416
+ console.print(
417
+ "[dim]Per Agent Skills spec: names must be lowercase alphanumeric "
418
+ "with hyphens only.\n"
419
+ "Examples: web-research, code-review, data-analysis[/dim]",
420
+ style=theme.MUTED,
421
+ )
422
+ raise SystemExit(1)
423
+
424
+ # Determine target directory
425
+ settings = Settings.from_environment()
426
+ if project:
427
+ if not settings.project_root:
428
+ console.print("[bold red]Error:[/bold red] Not in a project directory.")
429
+ console.print(
430
+ "[dim]Project skills require a .git directory "
431
+ "in the project root.[/dim]",
432
+ style=theme.MUTED,
433
+ )
434
+ raise SystemExit(1)
435
+ skills_dir = settings.ensure_project_skills_dir()
436
+ if skills_dir is None:
437
+ console.print(
438
+ "[bold red]Error:[/bold red] Could not create project skills directory."
439
+ )
440
+ raise SystemExit(1)
441
+ else:
442
+ skills_dir = settings.ensure_user_skills_dir(agent)
443
+
444
+ skill_dir = skills_dir / skill_name
445
+
446
+ # Validate the resolved path is within skills_dir
447
+ is_valid_path, path_error = _validate_skill_path(skill_dir, skills_dir)
448
+ if not is_valid_path:
449
+ console.print(f"[bold red]Error:[/bold red] {path_error}")
450
+ raise SystemExit(1)
451
+
452
+ if skill_dir.exists():
453
+ if output_format == "json":
454
+ from deepagents_code.output import write_json
455
+
456
+ write_json(
457
+ "skills create",
458
+ {
459
+ "name": skill_name,
460
+ "path": str(skill_dir),
461
+ "project": project,
462
+ "already_existed": True,
463
+ },
464
+ )
465
+ return
466
+ console.print(
467
+ f"Skill '{skill_name}' already exists at {skill_dir}",
468
+ style=theme.MUTED,
469
+ )
470
+ return
471
+
472
+ # Create skill directory
473
+ skill_dir.mkdir(parents=True, exist_ok=True)
474
+
475
+ template = _generate_template(skill_name)
476
+ skill_md = skill_dir / "SKILL.md"
477
+ skill_md.write_text(template)
478
+
479
+ if output_format == "json":
480
+ from deepagents_code.output import write_json
481
+
482
+ write_json(
483
+ "skills create",
484
+ {
485
+ "name": skill_name,
486
+ "path": str(skill_dir),
487
+ "project": project,
488
+ },
489
+ )
490
+ return
491
+
492
+ checkmark = get_glyphs().checkmark
493
+ console.print(
494
+ f"\n[bold]{checkmark} Skill '{skill_name}' created successfully![/bold]",
495
+ style=theme.PRIMARY,
496
+ )
497
+ console.print(f"Location: {skill_dir}\n", style=theme.MUTED)
498
+ console.print(
499
+ "[dim]Edit the SKILL.md file to customize:\n"
500
+ " 1. Update the description in YAML frontmatter\n"
501
+ " 2. Fill in the instructions and examples\n"
502
+ " 3. Add any supporting files (scripts, configs, etc.)\n"
503
+ "\n"
504
+ f" nano {skill_md}\n"
505
+ "\n"
506
+ " See examples/skills/ in the deepagents-code repo for example skills:\n"
507
+ " - web-research: Structured research workflow\n"
508
+ " - langgraph-docs: LangGraph documentation lookup\n"
509
+ "\n"
510
+ " Copy an example:\n"
511
+ " cp -r examples/skills/web-research ~/.deepagents/agent/skills/\n",
512
+ style=theme.MUTED,
513
+ )
514
+
515
+
516
+ def _info(
517
+ skill_name: str,
518
+ *,
519
+ agent: str = "agent",
520
+ project: bool = False,
521
+ output_format: OutputFormat = "text",
522
+ ) -> None:
523
+ """Show detailed information about a specific skill.
524
+
525
+ Args:
526
+ skill_name: Name of the skill to show info for.
527
+ agent: Agent identifier for skills (default: agent).
528
+ project: If True, only search in project skills.
529
+ If False, search in both user and project skills.
530
+ output_format: Output format — `'text'` (Rich) or `'json'`.
531
+
532
+ Raises:
533
+ SystemExit: If the skill is not found or not in a project directory.
534
+ """
535
+ from rich.markup import escape as escape_markup
536
+
537
+ from deepagents_code.config import Settings, console
538
+ from deepagents_code.skills.load import list_skills
539
+
540
+ settings = Settings.from_environment()
541
+ user_skills_dir = settings.get_user_skills_dir(agent)
542
+ project_skills_dir = settings.get_project_skills_dir()
543
+ user_agent_skills_dir = settings.get_user_agent_skills_dir()
544
+ project_agent_skills_dir = settings.get_project_agent_skills_dir()
545
+
546
+ # Load skills based on --project flag
547
+ if project:
548
+ if not project_skills_dir:
549
+ console.print("[bold red]Error:[/bold red] Not in a project directory.")
550
+ raise SystemExit(1)
551
+ skills = list_skills(
552
+ user_skills_dir=None,
553
+ project_skills_dir=project_skills_dir,
554
+ user_agent_skills_dir=None,
555
+ project_agent_skills_dir=project_agent_skills_dir,
556
+ )
557
+ else:
558
+ skills = list_skills(
559
+ built_in_skills_dir=settings.get_built_in_skills_dir(),
560
+ user_skills_dir=user_skills_dir,
561
+ project_skills_dir=project_skills_dir,
562
+ user_agent_skills_dir=user_agent_skills_dir,
563
+ project_agent_skills_dir=project_agent_skills_dir,
564
+ )
565
+
566
+ # Find the skill
567
+ skill = next((s for s in skills if s["name"] == skill_name), None)
568
+
569
+ if not skill:
570
+ console.print(f"[bold red]Error:[/bold red] Skill '{skill_name}' not found.")
571
+ console.print("\n[dim]Available skills:[/dim]", style=theme.MUTED)
572
+ for s in skills:
573
+ console.print(f" - {s['name']}", style=theme.MUTED)
574
+ raise SystemExit(1)
575
+
576
+ if output_format == "json":
577
+ from deepagents_code.output import write_json
578
+
579
+ write_json("skills info", dict(skill))
580
+ return
581
+
582
+ # Read the full SKILL.md file
583
+ skill_path = Path(skill["path"])
584
+ skill_content = skill_path.read_text(encoding="utf-8")
585
+
586
+ # Determine source label
587
+ source_labels = {
588
+ "project": ("Project Skill", "green"),
589
+ "user": ("User Skill", "cyan"),
590
+ "built-in": ("Built-in Skill", "magenta"),
591
+ }
592
+ source_label, source_color = source_labels.get(skill["source"], ("Skill", "dim"))
593
+
594
+ # Check if this project skill shadows a user skill with the same name.
595
+ # This is a cosmetic hint — if the second list_skills() call fails
596
+ # (e.g. permission error reading user dirs) we silently skip the warning
597
+ # rather than crashing the entire `skills info` display.
598
+ shadowed_user_skill = False
599
+ if skill["source"] == "project" and not project:
600
+ try:
601
+ user_only = list_skills(
602
+ user_skills_dir=user_skills_dir,
603
+ project_skills_dir=None,
604
+ user_agent_skills_dir=user_agent_skills_dir,
605
+ project_agent_skills_dir=None,
606
+ )
607
+ shadowed_user_skill = any(s["name"] == skill_name for s in user_only)
608
+ except Exception: # noqa: BLE001, S110 # Shadow detection is cosmetic, safe to swallow
609
+ pass
610
+
611
+ console.print(
612
+ f"\n[bold]Skill: {escape_markup(skill['name'])}[/bold] "
613
+ f"[bold {source_color}]({source_label})[/bold {source_color}]\n",
614
+ style=theme.PRIMARY,
615
+ )
616
+ if shadowed_user_skill:
617
+ console.print(
618
+ f"[yellow]Note: Overrides user skill '{escape_markup(skill_name)}' "
619
+ "of the same name[/yellow]\n"
620
+ )
621
+ console.print(
622
+ f"[bold]Location:[/bold] {escape_markup(str(skill_path.parent))}/\n",
623
+ style=theme.MUTED,
624
+ )
625
+ console.print(
626
+ f"[bold]Description:[/bold] {escape_markup(skill['description'])}\n",
627
+ style=theme.MUTED,
628
+ )
629
+
630
+ # Show optional metadata fields
631
+ for label, value in _format_info_fields(skill):
632
+ console.print(
633
+ f"[bold]{label}:[/bold] {escape_markup(value)}\n",
634
+ style=theme.MUTED,
635
+ )
636
+
637
+ # List supporting files
638
+ skill_dir = skill_path.parent
639
+ supporting_files = [f for f in skill_dir.iterdir() if f.name != "SKILL.md"]
640
+
641
+ if supporting_files:
642
+ console.print("[bold]Supporting Files:[/bold]", style=theme.MUTED)
643
+ for file in supporting_files:
644
+ console.print(f" - {escape_markup(file.name)}", style=theme.MUTED)
645
+ console.print()
646
+
647
+ # Show the full SKILL.md content
648
+ console.print("[bold]Full SKILL.md Content:[/bold]\n", style=theme.PRIMARY)
649
+ console.print(skill_content, style=theme.MUTED)
650
+ console.print()
651
+
652
+
653
+ def _delete(
654
+ skill_name: str,
655
+ *,
656
+ agent: str = "agent",
657
+ project: bool = False,
658
+ force: bool = False,
659
+ dry_run: bool = False,
660
+ output_format: OutputFormat = "text",
661
+ ) -> None:
662
+ """Delete a skill directory after validation and optional user confirmation.
663
+
664
+ Validates the skill name, locates the skill in user or project directories,
665
+ confirms the deletion with the user (unless `force` is `True`), and
666
+ recursively removes the skill directory.
667
+
668
+ Args:
669
+ skill_name: Name of the skill to delete.
670
+ agent: Agent identifier for skills.
671
+ project: If `True`, only search in project skills.
672
+
673
+ If `False`, search in both user and project skills.
674
+ force: If `True`, skip confirmation prompt.
675
+ dry_run: If `True`, print what would be removed without deleting.
676
+ output_format: Output format — `'text'` (Rich) or `'json'`.
677
+
678
+ Raises:
679
+ SystemExit: If the deletion fails or a safety check is violated.
680
+ """
681
+ from rich.markup import escape as escape_markup
682
+
683
+ from deepagents_code.config import Settings, console, get_glyphs
684
+ from deepagents_code.skills.load import list_skills
685
+
686
+ # Validate skill name first (per Agent Skills spec)
687
+ is_valid, error_msg = _validate_name(skill_name)
688
+ if not is_valid:
689
+ console.print(f"[bold red]Error:[/bold red] Invalid skill name: {error_msg}")
690
+ raise SystemExit(1)
691
+
692
+ settings = Settings.from_environment()
693
+ user_skills_dir = settings.get_user_skills_dir(agent)
694
+ project_skills_dir = settings.get_project_skills_dir()
695
+ user_agent_skills_dir = settings.get_user_agent_skills_dir()
696
+ project_agent_skills_dir = settings.get_project_agent_skills_dir()
697
+
698
+ # Load skills based on --project flag
699
+ if project:
700
+ if not project_skills_dir:
701
+ console.print("[bold red]Error:[/bold red] Not in a project directory.")
702
+ raise SystemExit(1)
703
+ skills = list_skills(
704
+ user_skills_dir=None,
705
+ project_skills_dir=project_skills_dir,
706
+ user_agent_skills_dir=None,
707
+ project_agent_skills_dir=project_agent_skills_dir,
708
+ )
709
+ else:
710
+ skills = list_skills(
711
+ user_skills_dir=user_skills_dir,
712
+ project_skills_dir=project_skills_dir,
713
+ user_agent_skills_dir=user_agent_skills_dir,
714
+ project_agent_skills_dir=project_agent_skills_dir,
715
+ )
716
+
717
+ # Find the skill
718
+ skill = next((s for s in skills if s["name"] == skill_name), None)
719
+
720
+ if not skill:
721
+ console.print(f"[bold red]Error:[/bold red] Skill '{skill_name}' not found.")
722
+ console.print("\n[dim]Available skills:[/dim]", style=theme.MUTED)
723
+ for s in skills:
724
+ source_tag = "[project]" if s["source"] == "project" else "[user]"
725
+ console.print(f" - {s['name']} {source_tag}", style=theme.MUTED)
726
+ raise SystemExit(1)
727
+
728
+ skill_path = Path(skill["path"])
729
+ skill_dir = skill_path.parent
730
+
731
+ # Validate the path is safe to delete
732
+ base_dir = project_skills_dir if skill["source"] == "project" else user_skills_dir
733
+ if not base_dir:
734
+ console.print(
735
+ "[bold red]Error:[/bold red] Cannot determine base skills directory. "
736
+ "Refusing to delete."
737
+ )
738
+ raise SystemExit(1)
739
+ is_valid_path, path_error = _validate_skill_path(skill_dir, base_dir)
740
+ if not is_valid_path:
741
+ console.print(f"[bold red]Error:[/bold red] {path_error}")
742
+ raise SystemExit(1)
743
+
744
+ if dry_run:
745
+ if output_format == "json":
746
+ from deepagents_code.output import write_json
747
+
748
+ write_json(
749
+ "skills delete",
750
+ {
751
+ "name": skill_name,
752
+ "path": str(skill_dir),
753
+ "dry_run": True,
754
+ },
755
+ )
756
+ return
757
+ console.print(
758
+ f"Would delete skill '{skill_name}' at {skill_dir}",
759
+ )
760
+ console.print("No changes made.", style=theme.MUTED)
761
+ return
762
+
763
+ # Display confirmation summary (text mode only)
764
+ if output_format != "json":
765
+ source_label = "Project Skill" if skill["source"] == "project" else "User Skill"
766
+ source_color = "green" if skill["source"] == "project" else "cyan"
767
+
768
+ # Count files for the confirmation summary (display-only; a permission
769
+ # error in a subdirectory should not abort the entire delete flow).
770
+ try:
771
+ file_count = sum(1 for f in skill_dir.rglob("*") if f.is_file())
772
+ except OSError:
773
+ file_count = -1
774
+
775
+ console.print(
776
+ f"\n[bold]Skill:[/bold] {escape_markup(skill_name)}"
777
+ f" [bold {source_color}]({source_label})[/bold {source_color}]",
778
+ style=theme.PRIMARY,
779
+ )
780
+ console.print(
781
+ f"[bold]Location:[/bold] {escape_markup(str(skill_dir))}/",
782
+ style=theme.MUTED,
783
+ )
784
+ if file_count >= 0:
785
+ console.print(
786
+ f"[bold]Files:[/bold] {file_count} file(s) will be deleted\n",
787
+ style=theme.MUTED,
788
+ )
789
+ else:
790
+ console.print(
791
+ "[bold]Files:[/bold] (unable to count files)\n",
792
+ style=theme.MUTED,
793
+ )
794
+
795
+ # Confirmation (skip in JSON mode — no interactive prompt)
796
+ if not force and output_format != "json":
797
+ console.print(
798
+ "[yellow]Are you sure you want to delete this skill? (y/N)[/yellow] ",
799
+ end="",
800
+ )
801
+ try:
802
+ response = input().strip().lower()
803
+ except (EOFError, KeyboardInterrupt):
804
+ console.print("\n[dim]Cancelled.[/dim]")
805
+ return
806
+
807
+ if response not in {"y", "yes"}:
808
+ console.print("[dim]Cancelled.[/dim]")
809
+ return
810
+
811
+ # Re-validate immediately before deletion to narrow the TOCTOU window
812
+ # (the user may have paused at the confirmation prompt).
813
+ if skill_dir.is_symlink():
814
+ console.print(
815
+ "[bold red]Error:[/bold red] Skill directory is a symlink. "
816
+ "Refusing to delete for safety."
817
+ )
818
+ raise SystemExit(1)
819
+
820
+ is_valid_path, path_error = _validate_skill_path(skill_dir, base_dir)
821
+ if not is_valid_path:
822
+ console.print(f"[bold red]Error:[/bold red] {path_error}")
823
+ raise SystemExit(1)
824
+
825
+ # Delete the skill directory
826
+ try:
827
+ shutil.rmtree(skill_dir)
828
+ except OSError as e:
829
+ console.print(
830
+ f"[bold red]Error:[/bold red] Failed to fully delete skill: {e}\n"
831
+ f"[yellow]Warning:[/yellow] Some files may have been partially removed.\n"
832
+ f"Please inspect: {skill_dir}/"
833
+ )
834
+ raise SystemExit(1) from e
835
+
836
+ if output_format == "json":
837
+ from deepagents_code.output import write_json
838
+
839
+ write_json(
840
+ "skills delete",
841
+ {
842
+ "name": skill_name,
843
+ "path": str(skill_dir),
844
+ "deleted": True,
845
+ },
846
+ )
847
+ return
848
+
849
+ checkmark = get_glyphs().checkmark
850
+ console.print(
851
+ f"{checkmark} Skill '{skill_name}' deleted successfully!",
852
+ style=theme.PRIMARY,
853
+ )
854
+
855
+
856
+ def _trust(args: argparse.Namespace) -> None:
857
+ """Handle `skills trust list|revoke|clear`.
858
+
859
+ Args:
860
+ args: Parsed arguments with a `trust_command` attribute.
861
+
862
+ Raises:
863
+ SystemExit: If the trust store cannot be read, or trust entries cannot
864
+ be revoked or cleared.
865
+ """
866
+ from rich.markup import escape
867
+
868
+ from deepagents_code.config import console, get_glyphs
869
+ from deepagents_code.skills.trust import (
870
+ RevokeResult,
871
+ clear_trusted_skill_dirs,
872
+ list_trusted_skill_dir_entries,
873
+ revoke_skill_dir_trust,
874
+ )
875
+
876
+ command = getattr(args, "trust_command", None)
877
+ output_format = getattr(args, "output_format", "text")
878
+ checkmark = get_glyphs().checkmark
879
+
880
+ if command in {"list", "ls"}:
881
+ # Read strictly so an unreadable store surfaces as an error instead of
882
+ # falsely reporting "No trusted skill directories" — the whole point of
883
+ # the audit command is to show what is trusted so it can be revoked.
884
+ try:
885
+ entries = list_trusted_skill_dir_entries(strict=True)
886
+ except (OSError, ValueError) as exc:
887
+ console.print(
888
+ f"[bold red]Error:[/bold red] Could not read the skill trust "
889
+ f"store: {escape(str(exc))}"
890
+ )
891
+ raise SystemExit(1) from exc
892
+ if output_format == "json":
893
+ from deepagents_code.output import write_json
894
+
895
+ write_json(
896
+ "skills trust list",
897
+ [
898
+ {"dir": path, "trusted_at": trusted_at}
899
+ for path, trusted_at in entries
900
+ ],
901
+ )
902
+ return
903
+ if not entries:
904
+ console.print()
905
+ console.print("[yellow]No trusted skill directories.[/yellow]")
906
+ console.print(
907
+ "[dim]Directories are trusted when you approve a skill that "
908
+ "resolves outside the standard skill roots.[/dim]",
909
+ style=theme.MUTED,
910
+ )
911
+ console.print()
912
+ return
913
+ console.print(
914
+ "\n[bold]Trusted skill directories:[/bold]\n", style=theme.PRIMARY
915
+ )
916
+ for path, trusted_at in entries:
917
+ console.print(f" {escape(str(path))}")
918
+ if trusted_at:
919
+ console.print(
920
+ f" [dim]trusted {escape(trusted_at)}[/dim]", style=theme.MUTED
921
+ )
922
+ console.print()
923
+ elif command == "revoke":
924
+ target = args.dir
925
+ result = revoke_skill_dir_trust(target)
926
+ # An I/O/read failure is a hard error regardless of output format
927
+ # (matching `list`): print red and exit non-zero without emitting a
928
+ # success envelope a script might misread.
929
+ if result is RevokeResult.ERROR:
930
+ console.print(
931
+ "[bold red]Error:[/bold red] Could not revoke trust for: "
932
+ f"{escape(str(target))}"
933
+ )
934
+ raise SystemExit(1)
935
+ if output_format == "json":
936
+ from deepagents_code.output import write_json
937
+
938
+ write_json(
939
+ "skills trust revoke",
940
+ {"dir": str(target), "result": result.value},
941
+ )
942
+ return
943
+ # `ERROR` was handled above (early exit), so only `REMOVED`/`NOT_FOUND`
944
+ # remain. Match exhaustively with `assert_never` so adding a future
945
+ # `RevokeResult` member is a static error here rather than a silent
946
+ # success that prints nothing yet exits 0.
947
+ match result:
948
+ case RevokeResult.REMOVED:
949
+ console.print(
950
+ f"{checkmark} Revoked trust for: {escape(str(target))}",
951
+ style=theme.PRIMARY,
952
+ )
953
+ case RevokeResult.NOT_FOUND:
954
+ # Report honestly, not a false success.
955
+ console.print(
956
+ f"[yellow]No trust entry found for:[/yellow] {escape(str(target))}"
957
+ )
958
+ case _: # pragma: no cover - exhaustiveness guard
959
+ assert_never(result)
960
+ elif command == "clear":
961
+ if not clear_trusted_skill_dirs():
962
+ console.print(
963
+ "[bold red]Error:[/bold red] Could not clear trusted directories."
964
+ )
965
+ raise SystemExit(1)
966
+ if output_format == "json":
967
+ from deepagents_code.output import write_json
968
+
969
+ write_json("skills trust clear", {"cleared": True})
970
+ return
971
+ console.print(
972
+ f"{checkmark} Cleared all trusted skill directories.",
973
+ style=theme.PRIMARY,
974
+ )
975
+ else:
976
+ from deepagents_code.ui import show_skills_trust_help
977
+
978
+ show_skills_trust_help()
979
+
980
+
981
+ def setup_skills_parser(
982
+ subparsers: Any, # noqa: ANN401 # argparse subparsers uses dynamic typing
983
+ *,
984
+ make_help_action: Callable[[Callable[[], None]], type[argparse.Action]],
985
+ add_output_args: Callable[[argparse.ArgumentParser], None] | None = None,
986
+ ) -> argparse.ArgumentParser:
987
+ """Setup the skills subcommand parser with all its subcommands.
988
+
989
+ Each subcommand gets a dedicated help screen so that
990
+ `deepagents skills -h` shows skills-specific help, not the
991
+ global help.
992
+
993
+ Args:
994
+ subparsers: The parent subparsers object to add the skills parser to.
995
+ make_help_action: Factory that accepts a zero-argument help
996
+ callable and returns an argparse Action class wired to it.
997
+ add_output_args: Optional hook to add a shared `--json` flag.
998
+
999
+ Returns:
1000
+ The skills subparser for argument handling.
1001
+ """
1002
+
1003
+ # Lazy wrapper: defers ui import until the help action fires.
1004
+ def _lazy_help(fn_name: str) -> Callable[[], None]:
1005
+ def _show() -> None:
1006
+ from deepagents_code import ui
1007
+
1008
+ getattr(ui, fn_name)()
1009
+
1010
+ return _show
1011
+
1012
+ def help_parent(help_fn: Callable[[], None]) -> list[argparse.ArgumentParser]:
1013
+ parent = argparse.ArgumentParser(add_help=False)
1014
+ parent.add_argument("-h", "--help", action=make_help_action(help_fn))
1015
+ return [parent]
1016
+
1017
+ skills_parser = subparsers.add_parser(
1018
+ "skills",
1019
+ help="Manage agent skills",
1020
+ description="Manage agent skills - list, create, view, and delete skills.",
1021
+ add_help=False,
1022
+ parents=help_parent(_lazy_help("show_skills_help")),
1023
+ )
1024
+ if add_output_args is not None:
1025
+ add_output_args(skills_parser)
1026
+ skills_subparsers = skills_parser.add_subparsers(
1027
+ dest="skills_command", help="Skills command"
1028
+ )
1029
+
1030
+ # Skills list
1031
+ list_parser = skills_subparsers.add_parser(
1032
+ "list",
1033
+ aliases=["ls"],
1034
+ help="List all available skills",
1035
+ description=(
1036
+ "List skills from all four skill directories "
1037
+ "(user, user alias, project, project alias)."
1038
+ ),
1039
+ add_help=False,
1040
+ parents=help_parent(_lazy_help("show_skills_list_help")),
1041
+ )
1042
+ if add_output_args is not None:
1043
+ add_output_args(list_parser)
1044
+ list_parser.add_argument(
1045
+ "--agent",
1046
+ default="agent",
1047
+ help="Agent identifier for skills (default: agent)",
1048
+ )
1049
+ list_parser.add_argument(
1050
+ "--project",
1051
+ action="store_true",
1052
+ help="Show only project-level skills",
1053
+ )
1054
+
1055
+ # Skills create
1056
+ create_parser = skills_subparsers.add_parser(
1057
+ "create",
1058
+ help="Create a new skill",
1059
+ description=(
1060
+ "Create a new skill with a template SKILL.md file. "
1061
+ "By default, skills are created in "
1062
+ "~/.deepagents/<agent>/skills/. "
1063
+ "Use --project to create in the project's "
1064
+ ".deepagents/skills/ directory."
1065
+ ),
1066
+ add_help=False,
1067
+ parents=help_parent(_lazy_help("show_skills_create_help")),
1068
+ )
1069
+ if add_output_args is not None:
1070
+ add_output_args(create_parser)
1071
+ create_parser.add_argument(
1072
+ "name",
1073
+ help="Name of the skill to create (e.g., web-research)",
1074
+ )
1075
+ create_parser.add_argument(
1076
+ "--agent",
1077
+ default="agent",
1078
+ help="Agent identifier for skills (default: agent)",
1079
+ )
1080
+ create_parser.add_argument(
1081
+ "--project",
1082
+ action="store_true",
1083
+ help="Create skill in project directory instead of user directory",
1084
+ )
1085
+
1086
+ # Skills info
1087
+ info_parser = skills_subparsers.add_parser(
1088
+ "info",
1089
+ help="Show detailed information about a skill",
1090
+ description="Show detailed information about a specific skill",
1091
+ add_help=False,
1092
+ parents=help_parent(_lazy_help("show_skills_info_help")),
1093
+ )
1094
+ if add_output_args is not None:
1095
+ add_output_args(info_parser)
1096
+ info_parser.add_argument("name", help="Name of the skill to show info for")
1097
+ info_parser.add_argument(
1098
+ "--agent",
1099
+ default="agent",
1100
+ help="Agent identifier for skills (default: agent)",
1101
+ )
1102
+ info_parser.add_argument(
1103
+ "--project",
1104
+ action="store_true",
1105
+ help="Search only in project skills",
1106
+ )
1107
+
1108
+ # Skills delete
1109
+ delete_parser = skills_subparsers.add_parser(
1110
+ "delete",
1111
+ help="Delete a skill",
1112
+ description="Delete a skill directory and all its contents",
1113
+ add_help=False,
1114
+ parents=help_parent(_lazy_help("show_skills_delete_help")),
1115
+ )
1116
+ if add_output_args is not None:
1117
+ add_output_args(delete_parser)
1118
+ delete_parser.add_argument("name", help="Name of the skill to delete")
1119
+ delete_parser.add_argument(
1120
+ "--agent",
1121
+ default="agent",
1122
+ help="Agent identifier for skills (default: agent)",
1123
+ )
1124
+ delete_parser.add_argument(
1125
+ "--project",
1126
+ action="store_true",
1127
+ help="Search only in project skills",
1128
+ )
1129
+ delete_parser.add_argument(
1130
+ "-f",
1131
+ "--force",
1132
+ action="store_true",
1133
+ help="Skip confirmation prompt",
1134
+ )
1135
+ delete_parser.add_argument(
1136
+ "--dry-run",
1137
+ action="store_true",
1138
+ help="Show what would happen without making changes",
1139
+ )
1140
+
1141
+ # Skills trust — manage directories approved to be read outside the
1142
+ # standard skill roots (the persistent counterpart to the in-TUI prompt).
1143
+ trust_parser = skills_subparsers.add_parser(
1144
+ "trust",
1145
+ help="Manage trusted skill directories",
1146
+ description=(
1147
+ "List, revoke, or clear skill directories that have been trusted "
1148
+ "to be read even though they resolve outside the standard skill "
1149
+ "roots (for example, symlink targets approved at invocation time)."
1150
+ ),
1151
+ add_help=False,
1152
+ parents=help_parent(_lazy_help("show_skills_trust_help")),
1153
+ )
1154
+ if add_output_args is not None:
1155
+ add_output_args(trust_parser)
1156
+ trust_subparsers = trust_parser.add_subparsers(
1157
+ dest="trust_command", help="Trust command"
1158
+ )
1159
+ trust_list_parser = trust_subparsers.add_parser(
1160
+ "list",
1161
+ aliases=["ls"],
1162
+ help="List trusted skill directories",
1163
+ )
1164
+ if add_output_args is not None:
1165
+ add_output_args(trust_list_parser)
1166
+ revoke_parser = trust_subparsers.add_parser(
1167
+ "revoke",
1168
+ help="Revoke trust for a directory",
1169
+ )
1170
+ revoke_parser.add_argument("dir", help="Directory path to revoke")
1171
+ if add_output_args is not None:
1172
+ add_output_args(revoke_parser)
1173
+ clear_parser = trust_subparsers.add_parser(
1174
+ "clear",
1175
+ help="Remove all trusted skill directories",
1176
+ )
1177
+ if add_output_args is not None:
1178
+ add_output_args(clear_parser)
1179
+ return skills_parser
1180
+
1181
+
1182
+ def execute_skills_command(args: argparse.Namespace) -> None:
1183
+ """Execute skills subcommands based on parsed arguments.
1184
+
1185
+ Args:
1186
+ args: Parsed command line arguments with skills_command attribute
1187
+
1188
+ Raises:
1189
+ SystemExit: If the agent name is invalid.
1190
+ """
1191
+ from deepagents_code.config import console
1192
+
1193
+ # The `trust` subcommand manages directory paths, not agent-scoped skills,
1194
+ # so it has no `--agent` and is dispatched before agent validation.
1195
+ if args.skills_command == "trust":
1196
+ _trust(args)
1197
+ return
1198
+
1199
+ # validate agent argument
1200
+ if getattr(args, "agent", None):
1201
+ is_valid, error_msg = _validate_name(args.agent)
1202
+ if not is_valid:
1203
+ console.print(
1204
+ f"[bold red]Error:[/bold red] Invalid agent name: {error_msg}"
1205
+ )
1206
+ console.print(
1207
+ "[dim]Agent names must only contain letters, numbers, "
1208
+ "hyphens, and underscores.[/dim]",
1209
+ style=theme.MUTED,
1210
+ )
1211
+ raise SystemExit(1)
1212
+
1213
+ output_format = getattr(args, "output_format", "text")
1214
+
1215
+ # "ls" is an argparse alias for "list" — argparse stores the alias
1216
+ # as-is in the namespace, so we must match both values.
1217
+ if args.skills_command in {"list", "ls"}:
1218
+ _list(agent=args.agent, project=args.project, output_format=output_format)
1219
+ elif args.skills_command == "create":
1220
+ _create(
1221
+ args.name,
1222
+ agent=args.agent,
1223
+ project=args.project,
1224
+ output_format=output_format,
1225
+ )
1226
+ elif args.skills_command == "info":
1227
+ _info(
1228
+ args.name,
1229
+ agent=args.agent,
1230
+ project=args.project,
1231
+ output_format=output_format,
1232
+ )
1233
+ elif args.skills_command == "delete":
1234
+ _delete(
1235
+ args.name,
1236
+ agent=args.agent,
1237
+ project=args.project,
1238
+ force=args.force,
1239
+ dry_run=args.dry_run,
1240
+ output_format=output_format,
1241
+ )
1242
+ else:
1243
+ # No subcommand provided, show skills help screen
1244
+ from deepagents_code.ui import show_skills_help
1245
+
1246
+ show_skills_help()
1247
+
1248
+
1249
+ __all__ = [
1250
+ "execute_skills_command",
1251
+ "setup_skills_parser",
1252
+ ]