talkops-opscloud 0.3.0b1__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 (309) hide show
  1. opscloud/__init__.py +5 -0
  2. opscloud/__main__.py +6 -0
  3. opscloud/_constants.py +53 -0
  4. opscloud/_debug.py +145 -0
  5. opscloud/_debug_buffer.py +250 -0
  6. opscloud/_textual_patches.py +367 -0
  7. opscloud/_version.py +15 -0
  8. opscloud/agent/__init__.py +27 -0
  9. opscloud/agent/config.py +168 -0
  10. opscloud/agent/factory.py +1103 -0
  11. opscloud/approval_mode.py +44 -0
  12. opscloud/backend/__init__.py +49 -0
  13. opscloud/backend/composite.py +89 -0
  14. opscloud/backend/local.py +183 -0
  15. opscloud/backend/registry.py +79 -0
  16. opscloud/backend/sandbox_config.py +107 -0
  17. opscloud/backend/sandbox_factory.py +432 -0
  18. opscloud/built_in_skills/remember/SKILL.md +124 -0
  19. opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
  20. opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
  21. opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
  22. opscloud/cli/__init__.py +1 -0
  23. opscloud/cli/commands/__init__.py +15 -0
  24. opscloud/cli/commands/auth.py +45 -0
  25. opscloud/cli/commands/doctor.py +100 -0
  26. opscloud/cli/commands/mcp.py +137 -0
  27. opscloud/cli/commands/threads.py +51 -0
  28. opscloud/cli/interactive.py +154 -0
  29. opscloud/cli/main.py +903 -0
  30. opscloud/cli/non_interactive.py +425 -0
  31. opscloud/cli/server_manager.py +58 -0
  32. opscloud/commands/__init__.py +19 -0
  33. opscloud/commands/_base.py +110 -0
  34. opscloud/commands/_guards.py +52 -0
  35. opscloud/commands/_router.py +146 -0
  36. opscloud/commands/_types.py +36 -0
  37. opscloud/commands/core/__init__.py +47 -0
  38. opscloud/commands/core/auth.py +94 -0
  39. opscloud/commands/core/bug.py +46 -0
  40. opscloud/commands/core/clear.py +106 -0
  41. opscloud/commands/core/cloud.py +185 -0
  42. opscloud/commands/core/compact.py +128 -0
  43. opscloud/commands/core/config_cmd.py +120 -0
  44. opscloud/commands/core/context.py +220 -0
  45. opscloud/commands/core/cost.py +44 -0
  46. opscloud/commands/core/doctor.py +535 -0
  47. opscloud/commands/core/effort.py +108 -0
  48. opscloud/commands/core/exit_cmd.py +41 -0
  49. opscloud/commands/core/fast.py +148 -0
  50. opscloud/commands/core/help_cmd.py +94 -0
  51. opscloud/commands/core/mcp.py +134 -0
  52. opscloud/commands/core/model.py +109 -0
  53. opscloud/commands/core/permissions.py +196 -0
  54. opscloud/commands/core/plugins.py +92 -0
  55. opscloud/commands/core/pool.py +108 -0
  56. opscloud/commands/core/resume.py +97 -0
  57. opscloud/commands/core/skills.py +178 -0
  58. opscloud/commands/power/__init__.py +51 -0
  59. opscloud/commands/power/agents.py +98 -0
  60. opscloud/commands/power/btw.py +85 -0
  61. opscloud/commands/power/copy.py +97 -0
  62. opscloud/commands/power/goal.py +548 -0
  63. opscloud/commands/power/loop.py +295 -0
  64. opscloud/commands/power/memory.py +287 -0
  65. opscloud/commands/power/review.py +152 -0
  66. opscloud/commands/power/rubric.py +287 -0
  67. opscloud/commands/power/runtime.py +321 -0
  68. opscloud/commands/power/skill_creator.py +70 -0
  69. opscloud/commands/power/skill_invoke.py +165 -0
  70. opscloud/commands/power/tasks.py +209 -0
  71. opscloud/commands/power/trace.py +117 -0
  72. opscloud/commands/power/ui_toggles.py +135 -0
  73. opscloud/commands/power/version.py +52 -0
  74. opscloud/commands/registry.py +176 -0
  75. opscloud/config/__init__.py +114 -0
  76. opscloud/config/adapters/__init__.py +5 -0
  77. opscloud/config/adapters/postgres.py +776 -0
  78. opscloud/config/adapters/sqlite.py +769 -0
  79. opscloud/config/aws.py +248 -0
  80. opscloud/config/cloud_profiles.py +437 -0
  81. opscloud/config/env_vars.py +81 -0
  82. opscloud/config/langsmith.py +319 -0
  83. opscloud/config/manifest.py +817 -0
  84. opscloud/config/metadata.py +205 -0
  85. opscloud/config/paths.py +604 -0
  86. opscloud/config/plugins.py +130 -0
  87. opscloud/config/settings.py +834 -0
  88. opscloud/config/store.py +453 -0
  89. opscloud/config/store_factory.py +97 -0
  90. opscloud/config/toml_config.py +562 -0
  91. opscloud/editor.py +173 -0
  92. opscloud/exceptions.py +65 -0
  93. opscloud/file_ops.py +217 -0
  94. opscloud/hooks/__init__.py +139 -0
  95. opscloud/hooks/env.py +44 -0
  96. opscloud/hooks/interrupt.py +121 -0
  97. opscloud/hooks/models/__init__.py +109 -0
  98. opscloud/hooks/models/adapters.py +19 -0
  99. opscloud/hooks/models/config.py +79 -0
  100. opscloud/hooks/models/domain.py +434 -0
  101. opscloud/hooks/models/transport.py +37 -0
  102. opscloud/hooks/tools.py +231 -0
  103. opscloud/input.py +48 -0
  104. opscloud/integrations/__init__.py +80 -0
  105. opscloud/integrations/base.py +153 -0
  106. opscloud/integrations/content_filter.py +119 -0
  107. opscloud/integrations/event_bus.py +480 -0
  108. opscloud/integrations/hooks.py +238 -0
  109. opscloud/integrations/identity.py +181 -0
  110. opscloud/integrations/notifications.py +134 -0
  111. opscloud/integrations/stream_bridge.py +397 -0
  112. opscloud/integrations/thread_store.py +81 -0
  113. opscloud/json_types.py +20 -0
  114. opscloud/mcp/__init__.py +57 -0
  115. opscloud/mcp/config.py +203 -0
  116. opscloud/mcp/discovery.py +229 -0
  117. opscloud/mcp/mcp_info.py +109 -0
  118. opscloud/mcp/middleware.py +102 -0
  119. opscloud/mcp/preload.py +505 -0
  120. opscloud/mcp/raw_config.py +149 -0
  121. opscloud/mcp/semantic_profiler.py +305 -0
  122. opscloud/mcp/session_manager.py +727 -0
  123. opscloud/mcp/trust.py +105 -0
  124. opscloud/memory/__init__.py +27 -0
  125. opscloud/memory/branch.py +61 -0
  126. opscloud/memory/guard.py +19 -0
  127. opscloud/memory/onboarding.py +120 -0
  128. opscloud/memory/registry.py +134 -0
  129. opscloud/memory/store.py +223 -0
  130. opscloud/middleware/__init__.py +106 -0
  131. opscloud/middleware/_repository_bounds.py +459 -0
  132. opscloud/middleware/ask_user.py +152 -0
  133. opscloud/middleware/auto_mode.py +60 -0
  134. opscloud/middleware/auto_mode_hitl.py +2434 -0
  135. opscloud/middleware/compaction.py +566 -0
  136. opscloud/middleware/configurable_model.py +233 -0
  137. opscloud/middleware/cost_tracking.py +1021 -0
  138. opscloud/middleware/glm_stall_recovery.py +109 -0
  139. opscloud/middleware/goal_criteria.py +1234 -0
  140. opscloud/middleware/goal_state_notice.py +712 -0
  141. opscloud/middleware/goal_tools.py +407 -0
  142. opscloud/middleware/headless_mcp_guard.py +88 -0
  143. opscloud/middleware/jev_model_router.py +1018 -0
  144. opscloud/middleware/local_context.py +460 -0
  145. opscloud/middleware/mcp_context.py +151 -0
  146. opscloud/middleware/mcp_middleware.py +146 -0
  147. opscloud/middleware/memory_guard.py +393 -0
  148. opscloud/middleware/model_retry.py +205 -0
  149. opscloud/middleware/registry.py +137 -0
  150. opscloud/middleware/reliable_rubric.py +709 -0
  151. opscloud/middleware/resume_state.py +91 -0
  152. opscloud/middleware/server_hooks.py +1189 -0
  153. opscloud/middleware/shell_allow_list.py +90 -0
  154. opscloud/middleware/skills.py +382 -0
  155. opscloud/middleware/subagent_artifacts.py +84 -0
  156. opscloud/middleware/subagent_telemetry.py +261 -0
  157. opscloud/middleware/subagents.py +243 -0
  158. opscloud/middleware/tool_filter.py +384 -0
  159. opscloud/middleware/unified_system_message.py +62 -0
  160. opscloud/model/__init__.py +37 -0
  161. opscloud/model/config.py +2992 -0
  162. opscloud/model/factory.py +477 -0
  163. opscloud/model/pool.py +459 -0
  164. opscloud/model/reasoning.py +282 -0
  165. opscloud/offload.py +173 -0
  166. opscloud/output.py +64 -0
  167. opscloud/plugins/__init__.py +189 -0
  168. opscloud/plugins/_json.py +22 -0
  169. opscloud/plugins/adapters/__init__.py +13 -0
  170. opscloud/plugins/adapters/agents.py +112 -0
  171. opscloud/plugins/adapters/commands.py +190 -0
  172. opscloud/plugins/adapters/mcp.py +363 -0
  173. opscloud/plugins/adapters/skills.py +132 -0
  174. opscloud/plugins/commands_cli.py +541 -0
  175. opscloud/plugins/discovery.py +512 -0
  176. opscloud/plugins/manifest.py +335 -0
  177. opscloud/plugins/marketplace.py +893 -0
  178. opscloud/plugins/models.py +334 -0
  179. opscloud/plugins/project_plugins.py +317 -0
  180. opscloud/plugins/protocol.py +31 -0
  181. opscloud/plugins/store.py +905 -0
  182. opscloud/plugins/substitution.py +122 -0
  183. opscloud/project_utils.py +145 -0
  184. opscloud/prompts/__init__.py +37 -0
  185. opscloud/prompts/system.py +483 -0
  186. opscloud/prompts/templates/system_prompt.md +166 -0
  187. opscloud/prompts/types.py +54 -0
  188. opscloud/rubrics/__init__.py +42 -0
  189. opscloud/rubrics/evaluator.py +404 -0
  190. opscloud/rubrics/evidence_extractor.py +229 -0
  191. opscloud/rubrics/generator.py +239 -0
  192. opscloud/rubrics/jev_compiler.py +175 -0
  193. opscloud/rubrics/jev_grader.py +297 -0
  194. opscloud/rubrics/middleware.py +8 -0
  195. opscloud/schema/__init__.py +107 -0
  196. opscloud/schema/interrupts.py +981 -0
  197. opscloud/security/__init__.py +91 -0
  198. opscloud/security/approval_mode.py +528 -0
  199. opscloud/security/approval_mode_source.py +230 -0
  200. opscloud/security/cli_ast_evaluator.py +552 -0
  201. opscloud/security/jev_classifier.py +651 -0
  202. opscloud/security/shell_safety.py +340 -0
  203. opscloud/security/unicode_security.py +439 -0
  204. opscloud/security/url_validation.py +160 -0
  205. opscloud/server/__init__.py +13 -0
  206. opscloud/server/_server_config.py +158 -0
  207. opscloud/server/server.py +242 -0
  208. opscloud/server/server_graph.py +93 -0
  209. opscloud/skills/__init__.py +91 -0
  210. opscloud/skills/commands.py +1099 -0
  211. opscloud/skills/invocation.py +200 -0
  212. opscloud/skills/load.py +388 -0
  213. opscloud/skills/loader.py +23 -0
  214. opscloud/skills/merge.py +65 -0
  215. opscloud/skills/registry.py +263 -0
  216. opscloud/skills/sources.py +174 -0
  217. opscloud/skills/trust.py +417 -0
  218. opscloud/state/__init__.py +142 -0
  219. opscloud/state/base.py +154 -0
  220. opscloud/state/cloud_context.py +262 -0
  221. opscloud/state/goal_channels.py +124 -0
  222. opscloud/state/goal_state_limits.py +175 -0
  223. opscloud/state/resume_state.py +58 -0
  224. opscloud/state/service_context.py +281 -0
  225. opscloud/state/session.py +957 -0
  226. opscloud/state/state_migration.py +86 -0
  227. opscloud/subagents/__init__.py +30 -0
  228. opscloud/subagents/loader.py +331 -0
  229. opscloud/subagents/subagents_parser.py +117 -0
  230. opscloud/subagents/types.py +25 -0
  231. opscloud/tools/__init__.py +36 -0
  232. opscloud/tools/catalog.py +37 -0
  233. opscloud/tools/display.py +24 -0
  234. opscloud/tools/fetch_url.py +176 -0
  235. opscloud/tools/goal_tools.py +423 -0
  236. opscloud/tools/registry.py +105 -0
  237. opscloud/tools/thread.py +32 -0
  238. opscloud/tools/web_search.py +215 -0
  239. opscloud/ui/__init__.py +119 -0
  240. opscloud/ui/app.py +5400 -0
  241. opscloud/ui/app.tcss +479 -0
  242. opscloud/ui/clipboard.py +147 -0
  243. opscloud/ui/command_registry.py +482 -0
  244. opscloud/ui/message_store.py +152 -0
  245. opscloud/ui/permission_store.py +360 -0
  246. opscloud/ui/preferences.py +113 -0
  247. opscloud/ui/prompt_manager.py +96 -0
  248. opscloud/ui/remote_client.py +505 -0
  249. opscloud/ui/textual_adapter.py +1269 -0
  250. opscloud/ui/theme.py +728 -0
  251. opscloud/ui/ui_help.py +415 -0
  252. opscloud/ui/widgets/__init__.py +383 -0
  253. opscloud/ui/widgets/_ask_user_types.py +81 -0
  254. opscloud/ui/widgets/_copy_spans.py +52 -0
  255. opscloud/ui/widgets/_inline_prompt.py +290 -0
  256. opscloud/ui/widgets/_js_eval_display.py +139 -0
  257. opscloud/ui/widgets/_links.py +175 -0
  258. opscloud/ui/widgets/_tool_stream.py +308 -0
  259. opscloud/ui/widgets/agent_selector.py +297 -0
  260. opscloud/ui/widgets/approval.py +741 -0
  261. opscloud/ui/widgets/ask_user.py +539 -0
  262. opscloud/ui/widgets/auth.py +695 -0
  263. opscloud/ui/widgets/auth_manager.py +214 -0
  264. opscloud/ui/widgets/auto_mode_notice.py +146 -0
  265. opscloud/ui/widgets/autocomplete.py +709 -0
  266. opscloud/ui/widgets/chat_input.py +805 -0
  267. opscloud/ui/widgets/cloud_selector.py +528 -0
  268. opscloud/ui/widgets/config_manager.py +652 -0
  269. opscloud/ui/widgets/debug_console.py +1269 -0
  270. opscloud/ui/widgets/devops_renderers.py +184 -0
  271. opscloud/ui/widgets/diff.py +237 -0
  272. opscloud/ui/widgets/effort_selector.py +166 -0
  273. opscloud/ui/widgets/goal_review.py +612 -0
  274. opscloud/ui/widgets/goal_status.py +57 -0
  275. opscloud/ui/widgets/infra_panel.py +141 -0
  276. opscloud/ui/widgets/install_confirm.py +114 -0
  277. opscloud/ui/widgets/loading.py +170 -0
  278. opscloud/ui/widgets/mcp_viewer.py +1204 -0
  279. opscloud/ui/widgets/messages.py +4461 -0
  280. opscloud/ui/widgets/model_selector.py +863 -0
  281. opscloud/ui/widgets/notification_center.py +108 -0
  282. opscloud/ui/widgets/notification_settings.py +119 -0
  283. opscloud/ui/widgets/operation_card.py +106 -0
  284. opscloud/ui/widgets/permissions_manager.py +667 -0
  285. opscloud/ui/widgets/plugin_manager.py +1212 -0
  286. opscloud/ui/widgets/pool_selector.py +376 -0
  287. opscloud/ui/widgets/preamble.py +53 -0
  288. opscloud/ui/widgets/skills_viewer.py +432 -0
  289. opscloud/ui/widgets/status.py +1037 -0
  290. opscloud/ui/widgets/subagent_panel.py +927 -0
  291. opscloud/ui/widgets/theme_selector.py +232 -0
  292. opscloud/ui/widgets/thread_selector.py +358 -0
  293. opscloud/ui/widgets/toast.py +31 -0
  294. opscloud/ui/widgets/tool_display.py +325 -0
  295. opscloud/ui/widgets/tool_renderers.py +234 -0
  296. opscloud/ui/widgets/tool_widgets.py +278 -0
  297. opscloud/ui/widgets/welcome.py +421 -0
  298. opscloud/ui/widgets/welcome_popup.py +102 -0
  299. opscloud/utils/__init__.py +24 -0
  300. opscloud/utils/cost_estimation.py +169 -0
  301. opscloud/utils/git.py +390 -0
  302. opscloud/utils/logger.py +571 -0
  303. opscloud/utils/session_stats.py +115 -0
  304. opscloud/utils/startup_error.py +37 -0
  305. talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
  306. talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
  307. talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
  308. talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
  309. talkops_opscloud-0.3.0b1.dist-info/licenses/LICENSE +175 -0
@@ -0,0 +1,1099 @@
1
+ """CLI commands for skill management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+ import shutil
8
+ from typing import TYPE_CHECKING, Any, assert_never
9
+
10
+ from rich.console import Console
11
+ from rich.markup import escape as escape_markup
12
+
13
+ from opscloud.config.paths import (
14
+ PATHS,
15
+ display_path,
16
+ ensure_project_skills_dir,
17
+ ensure_user_skills_dir,
18
+ get_built_in_skills_dir,
19
+ get_project_agent_skills_dir,
20
+ get_project_skills_dir,
21
+ get_user_agent_skills_dir,
22
+ get_user_skills_dir,
23
+ )
24
+ from opscloud.config.settings import get_glyphs, settings
25
+ from opscloud.output import OutputFormat, add_json_output_arg, write_json
26
+ from opscloud.ui.theme import DC_CYAN, DC_GREEN, DC_MUTED, DC_PURPLE, DC_TEAL
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Callable
30
+
31
+ from deepagents.middleware.skills import SkillMetadata
32
+
33
+ console = Console()
34
+
35
+ PRIMARY = DC_TEAL
36
+ MUTED = DC_MUTED
37
+ MAX_SKILL_NAME_LENGTH = 64
38
+
39
+
40
+ def _validate_name(name: str) -> tuple[bool, str]:
41
+ """Validate name per Agent Skills spec.
42
+
43
+ Requirements:
44
+ - Max 64 characters
45
+ - Unicode lowercase alphanumeric and hyphens only
46
+ - Cannot start or end with hyphen
47
+ - No consecutive hyphens
48
+ - No path traversal sequences
49
+ """
50
+ if not name or not name.strip():
51
+ return False, "cannot be empty"
52
+
53
+ if len(name) > MAX_SKILL_NAME_LENGTH:
54
+ return False, "cannot exceed 64 characters"
55
+
56
+ if ".." in name or "/" in name or "\\" in name:
57
+ return False, "cannot contain path components"
58
+
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
+ for c in name:
66
+ if c == "-":
67
+ continue
68
+ if (c.isalpha() and c.islower()) or c.isdigit():
69
+ continue
70
+ return (
71
+ False,
72
+ "must be lowercase alphanumeric with single hyphens only",
73
+ )
74
+
75
+ return True, ""
76
+
77
+
78
+ def _validate_skill_path(skill_dir: Path, base_dir: Path) -> tuple[bool, str]:
79
+ """Validate that the resolved skill directory is within the base directory."""
80
+ try:
81
+ resolved_skill = skill_dir.resolve()
82
+ resolved_base = base_dir.resolve()
83
+ if not resolved_skill.is_relative_to(resolved_base):
84
+ return False, f"Skill directory must be within {base_dir}"
85
+ except (OSError, RuntimeError) as e:
86
+ return False, f"Invalid path: {e}"
87
+ else:
88
+ return True, ""
89
+
90
+
91
+ def _format_info_fields(skill: SkillMetadata | dict[str, Any]) -> list[tuple[str, str]]:
92
+ """Extract non-empty optional metadata fields for display."""
93
+ fields: list[tuple[str, str]] = []
94
+ license_val = skill.get("license")
95
+ if license_val:
96
+ fields.append(("License", str(license_val)))
97
+ compat_val = skill.get("compatibility")
98
+ if compat_val:
99
+ fields.append(("Compatibility", str(compat_val)))
100
+ if skill.get("allowed_tools"):
101
+ fields.append(
102
+ ("Allowed Tools", ", ".join(str(t) for t in skill["allowed_tools"]))
103
+ )
104
+ meta = skill.get("metadata")
105
+ if meta and isinstance(meta, dict):
106
+ formatted = ", ".join(f"{k}={v}" for k, v in meta.items())
107
+ fields.append(("Metadata", formatted))
108
+ return fields
109
+
110
+
111
+ def _list(
112
+ agent: str = "opscloud",
113
+ *,
114
+ project: bool = False,
115
+ output_format: OutputFormat = "text",
116
+ ) -> None:
117
+ """List all available skills for the specified agent."""
118
+ from opscloud.skills.load import list_skills
119
+
120
+ effective_project_root = settings.project_root
121
+ user_skills_dir = get_user_skills_dir(agent)
122
+ project_skills_dir = get_project_skills_dir(effective_project_root)
123
+ user_agent_skills_dir = get_user_agent_skills_dir()
124
+ project_agent_skills_dir = get_project_agent_skills_dir(effective_project_root)
125
+
126
+ if project:
127
+ if not project_skills_dir:
128
+ if output_format == "json":
129
+ write_json("skills list", [])
130
+ return
131
+ console.print("[yellow]Not in a project directory.[/yellow]")
132
+ console.print(
133
+ "[dim]Project skills require a project root or .opscloud directory.[/dim]",
134
+ style=MUTED,
135
+ )
136
+ return
137
+
138
+ has_opscloud_skills = project_skills_dir.exists() and any(project_skills_dir.iterdir())
139
+ has_agent_skills = (
140
+ project_agent_skills_dir is not None
141
+ and project_agent_skills_dir.exists()
142
+ and any(project_agent_skills_dir.iterdir())
143
+ )
144
+
145
+ if not has_opscloud_skills and not has_agent_skills:
146
+ if output_format == "json":
147
+ write_json("skills list", [])
148
+ return
149
+ project_skills_display = escape_markup(display_path(project_skills_dir))
150
+ console.print("[yellow]No project skills found.[/yellow]")
151
+ console.print(
152
+ f"[dim]Project skills will be created in {project_skills_display} "
153
+ "when you add them.[/dim]",
154
+ style=MUTED,
155
+ )
156
+ console.print(
157
+ "\n[dim]Create a project skill:\n"
158
+ " opscloud skills create my-skill --project[/dim]",
159
+ style=MUTED,
160
+ )
161
+ return
162
+
163
+ skills = list_skills(
164
+ built_in_skills_dir=None,
165
+ user_skills_dir=None,
166
+ project_skills_dir=project_skills_dir,
167
+ user_agent_skills_dir=None,
168
+ project_agent_skills_dir=project_agent_skills_dir,
169
+ project_root=effective_project_root,
170
+ )
171
+
172
+ if output_format == "json":
173
+ write_json("skills list", [dict(s) for s in skills])
174
+ return
175
+
176
+ console.print(f"\n[bold {PRIMARY}]Project Skills:[/bold {PRIMARY}]\n")
177
+ else:
178
+ skills = list_skills(
179
+ built_in_skills_dir=get_built_in_skills_dir(),
180
+ user_skills_dir=user_skills_dir,
181
+ project_skills_dir=project_skills_dir,
182
+ user_agent_skills_dir=user_agent_skills_dir,
183
+ project_agent_skills_dir=project_agent_skills_dir,
184
+ project_root=effective_project_root,
185
+ )
186
+
187
+ if output_format == "json":
188
+ write_json("skills list", [dict(s) for s in skills])
189
+ return
190
+
191
+ if not skills:
192
+ user_skills_display = escape_markup(display_path(user_skills_dir))
193
+ console.print()
194
+ console.print("[yellow]No skills found.[/yellow]")
195
+ console.print()
196
+ console.print(
197
+ "[dim]Skills are loaded from these directories "
198
+ "(highest precedence first):\n"
199
+ " 1. .agents/skills/ project skills\n"
200
+ " 2. .opscloud/skills/ project skills (alias)\n"
201
+ " 3. ~/.agents/skills/ user skills\n"
202
+ f" 4. {user_skills_display} user skills (alias)\n"
203
+ " 5. <package>/built_in_skills/ built-in skills[/dim]",
204
+ style=MUTED,
205
+ )
206
+ console.print(
207
+ "\n[dim]Create your first skill:\n opscloud skills create my-skill[/dim]",
208
+ style=MUTED,
209
+ )
210
+ return
211
+
212
+ console.print(f"\n[bold {PRIMARY}]Available Skills:[/bold {PRIMARY}]\n")
213
+
214
+ user_skills = [s for s in skills if s.get("source") == "user"]
215
+ project_skills_list = [s for s in skills if s.get("source") == "project"]
216
+ plugin_skills_list = [s for s in skills if s.get("source") == "plugin"]
217
+ built_in_skills_list = [s for s in skills if s.get("source") == "built-in"]
218
+
219
+ bullet = get_glyphs().bullet
220
+
221
+ if user_skills and not project:
222
+ console.print(f"[bold {DC_CYAN}]User Skills:[/bold {DC_CYAN}]")
223
+ for skill in user_skills:
224
+ skill_path = Path(skill["path"])
225
+ name = escape_markup(skill["name"])
226
+ console.print(f" {bullet} [bold]{name}[/bold]")
227
+ console.print(
228
+ f" {escape_markup(display_path(skill_path.parent))}/",
229
+ style=MUTED,
230
+ )
231
+ console.print()
232
+ console.print(
233
+ f" {escape_markup(skill.get('description', ''))}",
234
+ style=MUTED,
235
+ )
236
+ console.print()
237
+
238
+ if project_skills_list:
239
+ if not project and user_skills:
240
+ console.print()
241
+ console.print(f"[bold {DC_GREEN}]Project Skills:[/bold {DC_GREEN}]")
242
+ for skill in project_skills_list:
243
+ skill_path = Path(skill["path"])
244
+ name = escape_markup(skill["name"])
245
+ console.print(f" {bullet} [bold]{name}[/bold]")
246
+ console.print(
247
+ f" {escape_markup(display_path(skill_path.parent))}/",
248
+ style=MUTED,
249
+ )
250
+ console.print()
251
+ console.print(
252
+ f" {escape_markup(skill.get('description', ''))}",
253
+ style=MUTED,
254
+ )
255
+ console.print()
256
+
257
+ if plugin_skills_list and not project:
258
+ if user_skills or project_skills_list:
259
+ console.print()
260
+ console.print(f"[bold {DC_PURPLE}]Plugin Skills:[/bold {DC_PURPLE}]")
261
+ for skill in plugin_skills_list:
262
+ skill_path = Path(skill["path"])
263
+ name = escape_markup(skill["name"])
264
+ console.print(f" {bullet} [bold]{name}[/bold]")
265
+ console.print(
266
+ f" {escape_markup(display_path(skill_path.parent))}/",
267
+ style=MUTED,
268
+ )
269
+ console.print()
270
+ console.print(
271
+ f" {escape_markup(skill.get('description', ''))}",
272
+ style=MUTED,
273
+ )
274
+ console.print()
275
+
276
+ if built_in_skills_list and not project:
277
+ if user_skills or project_skills_list or plugin_skills_list:
278
+ console.print()
279
+ console.print(f"[bold {DC_PURPLE}]Built-in Skills:[/bold {DC_PURPLE}]")
280
+ for skill in built_in_skills_list:
281
+ name = escape_markup(skill["name"])
282
+ console.print(f" {bullet} [bold]{name}[/bold]")
283
+ console.print()
284
+ console.print(
285
+ f" {escape_markup(skill.get('description', ''))}",
286
+ style=MUTED,
287
+ )
288
+ console.print()
289
+
290
+
291
+ def _generate_template(skill_name: str) -> str:
292
+ """Generate a `SKILL.md` template for a new skill per Agent Skills spec."""
293
+ title = skill_name.title().replace("-", " ")
294
+ description = (
295
+ "TODO: Explain what this skill does and when to use it. "
296
+ "Include specific triggers — scenarios, file types, CLI commands, or phrases "
297
+ "that should activate this skill. Example: 'Diagnose and remediate AWS ECS "
298
+ "task crash loops. Use when the user asks about ECS task failures or service instability.'"
299
+ )
300
+ return f"""---
301
+ name: {skill_name}
302
+ description: "{description}"
303
+ # Optional fields per Agent Skills spec:
304
+ # license: Apache-2.0
305
+ # compatibility: Designed for OpsCloud
306
+ # metadata:
307
+ # author: your-org
308
+ # version: "1.0"
309
+ # allowed-tools: execute read_file write_file
310
+ ---
311
+
312
+ # {title}
313
+
314
+ ## Overview
315
+
316
+ [TODO: 1-2 sentences explaining what this skill enables]
317
+
318
+ ## Instructions
319
+
320
+ ### Step 1: [First Action]
321
+ [Explain what to inspect or run first]
322
+
323
+ ### Step 2: [Second Action]
324
+ [Explain analysis or mutation steps]
325
+
326
+ ### Step 3: [Final Action]
327
+ [Explain validation and verification steps]
328
+
329
+ ## Best Practices
330
+
331
+ - [Best practice 1]
332
+ - [Best practice 2]
333
+ - [Best practice 3]
334
+
335
+ ## Examples
336
+
337
+ ### Example 1: [Scenario Name]
338
+
339
+ **User Request:** "[Example user request]"
340
+
341
+ **Approach:**
342
+ 1. [Step-by-step breakdown]
343
+ 2. [Using tools and commands]
344
+ 3. [Expected outcome]
345
+ """
346
+
347
+
348
+ def _create(
349
+ skill_name: str,
350
+ agent: str = "opscloud",
351
+ project: bool = False,
352
+ *,
353
+ output_format: OutputFormat = "text",
354
+ ) -> None:
355
+ """Create a new skill with a template SKILL.md file."""
356
+ is_valid, error_msg = _validate_name(skill_name)
357
+ if not is_valid:
358
+ console.print(f"[bold red]Error:[/bold red] Invalid skill name: {error_msg}")
359
+ console.print(
360
+ "[dim]Per Agent Skills spec: names must be lowercase alphanumeric "
361
+ "with hyphens only.\n"
362
+ "Examples: ecs-debugger, terraform-state-fix, cost-optimizer[/dim]",
363
+ style=MUTED,
364
+ )
365
+ raise SystemExit(1)
366
+
367
+ effective_project_root = settings.project_root
368
+ if project:
369
+ if not effective_project_root:
370
+ console.print("[bold red]Error:[/bold red] Not in a project directory.")
371
+ raise SystemExit(1)
372
+ skills_dir = ensure_project_skills_dir(effective_project_root)
373
+ if skills_dir is None:
374
+ console.print(
375
+ "[bold red]Error:[/bold red] Could not create project skills directory."
376
+ )
377
+ raise SystemExit(1)
378
+ else:
379
+ skills_dir = ensure_user_skills_dir(agent)
380
+
381
+ skill_dir = skills_dir / skill_name
382
+
383
+ is_valid_path, path_error = _validate_skill_path(skill_dir, skills_dir)
384
+ if not is_valid_path:
385
+ console.print(f"[bold red]Error:[/bold red] {path_error}")
386
+ raise SystemExit(1)
387
+
388
+ if skill_dir.exists():
389
+ if output_format == "json":
390
+ write_json(
391
+ "skills create",
392
+ {
393
+ "name": skill_name,
394
+ "path": str(skill_dir),
395
+ "project": project,
396
+ "already_existed": True,
397
+ },
398
+ )
399
+ return
400
+ console.print(
401
+ f"Skill '{skill_name}' already exists at {display_path(skill_dir)}",
402
+ style=MUTED,
403
+ )
404
+ return
405
+
406
+ skill_dir.mkdir(parents=True, exist_ok=True)
407
+ template = _generate_template(skill_name)
408
+ skill_md = skill_dir / "SKILL.md"
409
+ skill_md.write_text(template, encoding="utf-8")
410
+
411
+ if output_format == "json":
412
+ write_json(
413
+ "skills create",
414
+ {
415
+ "name": skill_name,
416
+ "path": str(skill_dir),
417
+ "project": project,
418
+ },
419
+ )
420
+ return
421
+
422
+ checkmark = get_glyphs().checkmark
423
+ skill_dir_display = escape_markup(display_path(skill_dir))
424
+ skill_md_display = escape_markup(display_path(skill_md))
425
+ console.print(
426
+ f"\n[bold {PRIMARY}]{checkmark} Skill '{skill_name}' created successfully![/bold {PRIMARY}]"
427
+ )
428
+ console.print(f"Location: {skill_dir_display}\n", style=MUTED)
429
+ console.print(
430
+ "[dim]Edit the SKILL.md file to customize:\n"
431
+ " 1. Update the description in YAML frontmatter with activation triggers\n"
432
+ " 2. Fill in instructions, best practices, and examples\n"
433
+ " 3. Add any supporting files (scripts, configs, templates)\n"
434
+ "\n"
435
+ f" nano {skill_md_display}\n",
436
+ style=MUTED,
437
+ )
438
+
439
+
440
+ def _info(
441
+ skill_name: str,
442
+ *,
443
+ agent: str = "opscloud",
444
+ project: bool = False,
445
+ output_format: OutputFormat = "text",
446
+ ) -> None:
447
+ """Show detailed information about a specific skill."""
448
+ from opscloud.skills.load import list_skills
449
+
450
+ effective_project_root = settings.project_root
451
+ user_skills_dir = get_user_skills_dir(agent)
452
+ project_skills_dir = get_project_skills_dir(effective_project_root)
453
+ user_agent_skills_dir = get_user_agent_skills_dir()
454
+ project_agent_skills_dir = get_project_agent_skills_dir(effective_project_root)
455
+
456
+ if project:
457
+ if not project_skills_dir:
458
+ console.print("[bold red]Error:[/bold red] Not in a project directory.")
459
+ raise SystemExit(1)
460
+ skills = list_skills(
461
+ built_in_skills_dir=None,
462
+ user_skills_dir=None,
463
+ project_skills_dir=project_skills_dir,
464
+ user_agent_skills_dir=None,
465
+ project_agent_skills_dir=project_agent_skills_dir,
466
+ project_root=effective_project_root,
467
+ )
468
+ else:
469
+ skills = list_skills(
470
+ built_in_skills_dir=get_built_in_skills_dir(),
471
+ user_skills_dir=user_skills_dir,
472
+ project_skills_dir=project_skills_dir,
473
+ user_agent_skills_dir=user_agent_skills_dir,
474
+ project_agent_skills_dir=project_agent_skills_dir,
475
+ project_root=effective_project_root,
476
+ )
477
+
478
+ normalized = skill_name.strip().lower()
479
+ skill = next(
480
+ (
481
+ s for s in skills
482
+ if s.get("name", "").lower() == normalized
483
+ or (":" in s.get("name", "") and s.get("name", "").rsplit(":", 1)[-1].lower() == normalized)
484
+ ),
485
+ None,
486
+ )
487
+
488
+ if not skill:
489
+ console.print(f"[bold red]Error:[/bold red] Skill '{skill_name}' not found.")
490
+ console.print("\n[dim]Available skills:[/dim]", style=MUTED)
491
+ for s in skills:
492
+ console.print(f" - {s['name']}", style=MUTED)
493
+ raise SystemExit(1)
494
+
495
+ if output_format == "json":
496
+ write_json("skills info", dict(skill))
497
+ return
498
+
499
+ skill_path = Path(skill["path"])
500
+ try:
501
+ skill_content = skill_path.read_text(encoding="utf-8")
502
+ except Exception:
503
+ skill_content = "(Unable to read SKILL.md content)"
504
+
505
+ source_labels = {
506
+ "project": ("Project Skill", DC_GREEN),
507
+ "user": ("User Skill", DC_CYAN),
508
+ "plugin": ("Plugin Skill", DC_PURPLE),
509
+ "built-in": ("Built-in Skill", DC_PURPLE),
510
+ }
511
+ source_label, source_color = source_labels.get(skill.get("source", ""), ("Skill", MUTED))
512
+
513
+ shadowed_user_skill = False
514
+ if skill.get("source") == "project" and not project:
515
+ try:
516
+ user_only = list_skills(
517
+ built_in_skills_dir=None,
518
+ user_skills_dir=user_skills_dir,
519
+ project_skills_dir=None,
520
+ user_agent_skills_dir=user_agent_skills_dir,
521
+ project_agent_skills_dir=None,
522
+ )
523
+ shadowed_user_skill = any(s["name"] == skill["name"] for s in user_only)
524
+ except Exception:
525
+ pass
526
+
527
+ console.print(
528
+ f"\n[bold {PRIMARY}]Skill: {escape_markup(skill['name'])}[/bold {PRIMARY}] "
529
+ f"[bold {source_color}]({source_label})[/bold {source_color}]\n"
530
+ )
531
+ if shadowed_user_skill:
532
+ console.print(
533
+ f"[yellow]Note: Overrides user skill '{escape_markup(skill['name'])}' "
534
+ "of the same name[/yellow]\n"
535
+ )
536
+ console.print(
537
+ f"[bold]Location:[/bold] {escape_markup(display_path(skill_path.parent))}/\n",
538
+ style=MUTED,
539
+ )
540
+ console.print(
541
+ f"[bold]Description:[/bold] {escape_markup(skill.get('description', ''))}\n",
542
+ style=MUTED,
543
+ )
544
+
545
+ for label, value in _format_info_fields(skill):
546
+ console.print(
547
+ f"[bold]{label}:[/bold] {escape_markup(value)}\n",
548
+ style=MUTED,
549
+ )
550
+
551
+ skill_dir = skill_path.parent
552
+ try:
553
+ supporting_files = [f for f in skill_dir.iterdir() if f.name != "SKILL.md"]
554
+ except OSError:
555
+ supporting_files = []
556
+
557
+ if supporting_files:
558
+ console.print("[bold]Supporting Files:[/bold]", style=MUTED)
559
+ for file in supporting_files:
560
+ console.print(f" - {escape_markup(file.name)}", style=MUTED)
561
+ console.print()
562
+
563
+ console.print(f"[bold {PRIMARY}]Full SKILL.md Content:[/bold {PRIMARY}]\n")
564
+ console.print(skill_content, style=MUTED)
565
+ console.print()
566
+
567
+
568
+ def _delete(
569
+ skill_name: str,
570
+ *,
571
+ agent: str = "opscloud",
572
+ project: bool = False,
573
+ force: bool = False,
574
+ dry_run: bool = False,
575
+ output_format: OutputFormat = "text",
576
+ ) -> None:
577
+ """Delete a skill directory after validation and confirmation."""
578
+ from opscloud.skills.load import list_skills
579
+
580
+ is_valid, error_msg = _validate_name(skill_name)
581
+ if not is_valid:
582
+ console.print(f"[bold red]Error:[/bold red] Invalid skill name: {error_msg}")
583
+ raise SystemExit(1)
584
+
585
+ effective_project_root = settings.project_root
586
+ user_skills_dir = get_user_skills_dir(agent)
587
+ project_skills_dir = get_project_skills_dir(effective_project_root)
588
+ user_agent_skills_dir = get_user_agent_skills_dir()
589
+ project_agent_skills_dir = get_project_agent_skills_dir(effective_project_root)
590
+
591
+ if project:
592
+ if not project_skills_dir:
593
+ console.print("[bold red]Error:[/bold red] Not in a project directory.")
594
+ raise SystemExit(1)
595
+ skills = list_skills(
596
+ built_in_skills_dir=None,
597
+ user_skills_dir=None,
598
+ project_skills_dir=project_skills_dir,
599
+ user_agent_skills_dir=None,
600
+ project_agent_skills_dir=project_agent_skills_dir,
601
+ project_root=effective_project_root,
602
+ )
603
+ else:
604
+ skills = list_skills(
605
+ built_in_skills_dir=None,
606
+ user_skills_dir=user_skills_dir,
607
+ project_skills_dir=project_skills_dir,
608
+ user_agent_skills_dir=user_agent_skills_dir,
609
+ project_agent_skills_dir=project_agent_skills_dir,
610
+ project_root=effective_project_root,
611
+ )
612
+
613
+ normalized = skill_name.strip().lower()
614
+ skill = next((s for s in skills if s["name"].lower() == normalized), None)
615
+
616
+ if not skill:
617
+ console.print(f"[bold red]Error:[/bold red] Skill '{skill_name}' not found.")
618
+ console.print("\n[dim]Available skills:[/dim]", style=MUTED)
619
+ for s in skills:
620
+ source_tag = f"[{s.get('source', '')}]"
621
+ console.print(f" - {s['name']} {source_tag}", style=MUTED)
622
+ raise SystemExit(1)
623
+
624
+ source = str(skill.get("source", ""))
625
+ if source == "built-in":
626
+ console.print("[bold red]Error:[/bold red] Cannot delete built-in skills.")
627
+ raise SystemExit(1)
628
+
629
+ skill_path = Path(skill["path"])
630
+ skill_dir = skill_path.parent
631
+
632
+ base_dir = project_skills_dir if source == "project" else user_skills_dir
633
+ if not base_dir:
634
+ console.print("[bold red]Error:[/bold red] Cannot determine base skills directory.")
635
+ raise SystemExit(1)
636
+ is_valid_path, path_error = _validate_skill_path(skill_dir, base_dir)
637
+ if not is_valid_path:
638
+ console.print(f"[bold red]Error:[/bold red] {path_error}")
639
+ raise SystemExit(1)
640
+
641
+ if dry_run:
642
+ if output_format == "json":
643
+ write_json(
644
+ "skills delete",
645
+ {
646
+ "name": skill_name,
647
+ "path": str(skill_dir),
648
+ "dry_run": True,
649
+ },
650
+ )
651
+ return
652
+ console.print(f"Would delete skill '{skill_name}' at {display_path(skill_dir)}")
653
+ console.print("No changes made.", style=MUTED)
654
+ return
655
+
656
+ if output_format != "json":
657
+ source_label = "Project Skill" if source == "project" else "User Skill"
658
+ source_color = DC_GREEN if source == "project" else DC_CYAN
659
+ try:
660
+ file_count = sum(1 for f in skill_dir.rglob("*") if f.is_file())
661
+ except OSError:
662
+ file_count = -1
663
+
664
+ console.print(
665
+ f"\n[bold {PRIMARY}]Skill:[/bold {PRIMARY}] {escape_markup(skill_name)}"
666
+ f" [bold {source_color}]({source_label})[/bold {source_color}]"
667
+ )
668
+ console.print(f"[bold]Location:[/bold] {escape_markup(display_path(skill_dir))}/", style=MUTED)
669
+ if file_count >= 0:
670
+ console.print(f"[bold]Files:[/bold] {file_count} file(s) will be deleted\n", style=MUTED)
671
+
672
+ if not force and output_format != "json":
673
+ console.print(
674
+ "[yellow]Are you sure you want to delete this skill? (y/N)[/yellow] ",
675
+ end="",
676
+ )
677
+ try:
678
+ response = input().strip().lower()
679
+ except (EOFError, KeyboardInterrupt):
680
+ console.print("\n[dim]Cancelled.[/dim]")
681
+ return
682
+
683
+ if response not in {"y", "yes"}:
684
+ console.print("[dim]Cancelled.[/dim]")
685
+ return
686
+
687
+ if skill_dir.is_symlink():
688
+ console.print(
689
+ "[bold red]Error:[/bold red] Skill directory is a symlink. Refusing to delete for safety."
690
+ )
691
+ raise SystemExit(1)
692
+
693
+ is_valid_path, path_error = _validate_skill_path(skill_dir, base_dir)
694
+ if not is_valid_path:
695
+ console.print(f"[bold red]Error:[/bold red] {path_error}")
696
+ raise SystemExit(1)
697
+
698
+ try:
699
+ shutil.rmtree(skill_dir)
700
+ except OSError as e:
701
+ console.print(f"[bold red]Error:[/bold red] Failed to fully delete skill: {e}")
702
+ raise SystemExit(1) from e
703
+
704
+ if output_format == "json":
705
+ write_json(
706
+ "skills delete",
707
+ {
708
+ "name": skill_name,
709
+ "path": str(skill_dir),
710
+ "deleted": True,
711
+ },
712
+ )
713
+ return
714
+
715
+ checkmark = get_glyphs().checkmark
716
+ console.print(
717
+ f"{checkmark} Skill '{skill_name}' deleted successfully!",
718
+ style=f"bold {PRIMARY}",
719
+ )
720
+
721
+
722
+ def _trust(args: argparse.Namespace) -> None:
723
+ """Handle `skills trust list|revoke|clear`."""
724
+ from opscloud.skills.trust import (
725
+ RevokeResult,
726
+ clear_trusted_skill_dirs,
727
+ list_trusted_skill_dir_entries,
728
+ revoke_skill_dir_trust,
729
+ )
730
+
731
+ command = getattr(args, "trust_command", None)
732
+ output_format: OutputFormat = (
733
+ "json" if getattr(args, "output_format", "text") == "json" else "text"
734
+ )
735
+ checkmark = get_glyphs().checkmark
736
+
737
+ if command in {"list", "ls"}:
738
+ try:
739
+ entries = list_trusted_skill_dir_entries(strict=True)
740
+ except (OSError, ValueError) as exc:
741
+ console.print(
742
+ f"[bold red]Error:[/bold red] Could not read the skill trust store: {escape_markup(str(exc))}"
743
+ )
744
+ raise SystemExit(1) from exc
745
+
746
+ if output_format == "json":
747
+ write_json(
748
+ "skills trust list",
749
+ [{"dir": path, "trusted_at": trusted_at} for path, trusted_at in entries],
750
+ )
751
+ return
752
+
753
+ if not entries:
754
+ console.print()
755
+ console.print("[yellow]No trusted skill directories.[/yellow]")
756
+ console.print(
757
+ "[dim]Directories are trusted when you approve a skill that "
758
+ "resolves outside the standard skill roots.[/dim]",
759
+ style=MUTED,
760
+ )
761
+ console.print()
762
+ return
763
+
764
+ console.print(f"\n[bold {PRIMARY}]Trusted skill directories:[/bold {PRIMARY}]\n")
765
+ for path, trusted_at in entries:
766
+ console.print(f" {escape_markup(display_path(path))}")
767
+ if trusted_at:
768
+ console.print(f" [dim]trusted {escape_markup(trusted_at)}[/dim]", style=MUTED)
769
+ console.print()
770
+
771
+ elif command == "revoke":
772
+ target = args.dir
773
+ result = revoke_skill_dir_trust(target)
774
+ if result is RevokeResult.ERROR:
775
+ console.print(
776
+ f"[bold red]Error:[/bold red] Could not revoke trust for: {escape_markup(str(target))}"
777
+ )
778
+ raise SystemExit(1)
779
+
780
+ if output_format == "json":
781
+ write_json("skills trust revoke", {"dir": str(target), "result": result.value})
782
+ return
783
+
784
+ match result:
785
+ case RevokeResult.REMOVED:
786
+ console.print(
787
+ f"{checkmark} Revoked trust for: {escape_markup(str(target))}",
788
+ style=f"bold {PRIMARY}",
789
+ )
790
+ case RevokeResult.NOT_FOUND:
791
+ console.print(f"[yellow]No trust entry found for:[/yellow] {escape_markup(str(target))}")
792
+ case _:
793
+ assert_never(result)
794
+
795
+ elif command == "clear":
796
+ if not clear_trusted_skill_dirs():
797
+ console.print("[bold red]Error:[/bold red] Could not clear trusted directories.")
798
+ raise SystemExit(1)
799
+
800
+ if output_format == "json":
801
+ write_json("skills trust clear", {"cleared": True})
802
+ return
803
+
804
+ console.print(
805
+ f"{checkmark} Cleared all trusted skill directories.",
806
+ style=f"bold {PRIMARY}",
807
+ )
808
+ else:
809
+ from opscloud.ui import ui_help
810
+
811
+ ui_help.show_skills_trust_help()
812
+
813
+
814
+ def setup_skills_parser(
815
+ subparsers: Any,
816
+ *,
817
+ make_help_action: Callable[[Callable[[], None]], type[argparse.Action]] | None = None,
818
+ add_output_args: Callable[[argparse.ArgumentParser], None] | None = None,
819
+ ) -> argparse.ArgumentParser:
820
+ """Setup the skills subcommand parser with all subcommands."""
821
+
822
+ def _lazy_help(fn_name: str) -> Callable[[], None]:
823
+ def _show() -> None:
824
+ from opscloud.ui import ui_help
825
+
826
+ fn = getattr(ui_help, fn_name, None)
827
+ if fn:
828
+ fn()
829
+ else:
830
+ ui_help.show_skills_help()
831
+
832
+ return _show
833
+
834
+ def help_parent(help_fn: Callable[[], None]) -> list[argparse.ArgumentParser]:
835
+ if make_help_action is None:
836
+ return []
837
+ parent = argparse.ArgumentParser(add_help=False)
838
+ parent.add_argument("-h", "--help", action=make_help_action(help_fn))
839
+ return [parent]
840
+
841
+ skills_parser = subparsers.add_parser(
842
+ "skills",
843
+ aliases=["skill"],
844
+ help="Manage agent skills",
845
+ description="Manage agent skills - list, create, view, and delete skills.",
846
+ add_help=make_help_action is None,
847
+ parents=help_parent(_lazy_help("show_skills_help")),
848
+ )
849
+ if add_output_args is not None:
850
+ add_output_args(skills_parser)
851
+ else:
852
+ add_json_output_arg(skills_parser)
853
+
854
+ skills_subparsers = skills_parser.add_subparsers(
855
+ dest="skills_command", help="Skills command"
856
+ )
857
+
858
+ # 1. list / ls
859
+ list_parser = skills_subparsers.add_parser(
860
+ "list",
861
+ aliases=["ls"],
862
+ help="List all available skills",
863
+ description="List skills from user, project, plugin, and built-in directories.",
864
+ add_help=make_help_action is None,
865
+ parents=help_parent(_lazy_help("show_skills_list_help")),
866
+ )
867
+ if add_output_args is not None:
868
+ add_output_args(list_parser)
869
+ else:
870
+ add_json_output_arg(list_parser)
871
+ list_parser.add_argument(
872
+ "--agent",
873
+ default="opscloud",
874
+ help="Agent identifier for skills (default: opscloud)",
875
+ )
876
+ list_parser.add_argument(
877
+ "--project",
878
+ action="store_true",
879
+ help="Show only project-level skills",
880
+ )
881
+
882
+ # 2. create
883
+ create_parser = skills_subparsers.add_parser(
884
+ "create",
885
+ help="Create a new skill",
886
+ description="Create a new skill directory with a template SKILL.md file.",
887
+ add_help=make_help_action is None,
888
+ parents=help_parent(_lazy_help("show_skills_create_help")),
889
+ )
890
+ if add_output_args is not None:
891
+ add_output_args(create_parser)
892
+ else:
893
+ add_json_output_arg(create_parser)
894
+ create_parser.add_argument(
895
+ "name",
896
+ help="Name of the skill to create (e.g. eks-debugger)",
897
+ )
898
+ create_parser.add_argument(
899
+ "--agent",
900
+ default="opscloud",
901
+ help="Agent identifier for skills (default: opscloud)",
902
+ )
903
+ create_parser.add_argument(
904
+ "--project",
905
+ action="store_true",
906
+ help="Create skill in project directory instead of user directory",
907
+ )
908
+
909
+ # 3. info
910
+ info_parser = skills_subparsers.add_parser(
911
+ "info",
912
+ help="Show detailed information about a skill",
913
+ description="Show detailed information about a specific skill",
914
+ add_help=make_help_action is None,
915
+ parents=help_parent(_lazy_help("show_skills_info_help")),
916
+ )
917
+ if add_output_args is not None:
918
+ add_output_args(info_parser)
919
+ else:
920
+ add_json_output_arg(info_parser)
921
+ info_parser.add_argument("name", help="Name of the skill to show info for")
922
+ info_parser.add_argument(
923
+ "--agent",
924
+ default="opscloud",
925
+ help="Agent identifier for skills (default: opscloud)",
926
+ )
927
+ info_parser.add_argument(
928
+ "--project",
929
+ action="store_true",
930
+ help="Search only in project skills",
931
+ )
932
+
933
+ # 4. delete
934
+ delete_parser = skills_subparsers.add_parser(
935
+ "delete",
936
+ aliases=["rm"],
937
+ help="Delete a skill",
938
+ description="Delete a skill directory and all its contents",
939
+ add_help=make_help_action is None,
940
+ parents=help_parent(_lazy_help("show_skills_delete_help")),
941
+ )
942
+ if add_output_args is not None:
943
+ add_output_args(delete_parser)
944
+ else:
945
+ add_json_output_arg(delete_parser)
946
+ delete_parser.add_argument("name", help="Name of the skill to delete")
947
+ delete_parser.add_argument(
948
+ "--agent",
949
+ default="opscloud",
950
+ help="Agent identifier for skills (default: opscloud)",
951
+ )
952
+ delete_parser.add_argument(
953
+ "--project",
954
+ action="store_true",
955
+ help="Search only in project skills",
956
+ )
957
+ delete_parser.add_argument(
958
+ "-f",
959
+ "--force",
960
+ action="store_true",
961
+ help="Skip confirmation prompt",
962
+ )
963
+ delete_parser.add_argument(
964
+ "--dry-run",
965
+ action="store_true",
966
+ help="Show what would happen without making changes",
967
+ )
968
+
969
+ # 5. trust
970
+ trust_parser = skills_subparsers.add_parser(
971
+ "trust",
972
+ help="Manage trusted skill directories",
973
+ description="List, revoke, or clear trusted skill directories outside standard roots.",
974
+ add_help=make_help_action is None,
975
+ parents=help_parent(_lazy_help("show_skills_trust_help")),
976
+ )
977
+ if add_output_args is not None:
978
+ add_output_args(trust_parser)
979
+ else:
980
+ add_json_output_arg(trust_parser)
981
+ trust_subparsers = trust_parser.add_subparsers(
982
+ dest="trust_command", help="Trust command"
983
+ )
984
+ trust_list = trust_subparsers.add_parser(
985
+ "list",
986
+ aliases=["ls"],
987
+ help="List trusted skill directories",
988
+ )
989
+ add_json_output_arg(trust_list)
990
+
991
+ revoke_parser = trust_subparsers.add_parser(
992
+ "revoke",
993
+ help="Revoke trust for a directory",
994
+ )
995
+ revoke_parser.add_argument("dir", help="Directory path to revoke")
996
+ add_json_output_arg(revoke_parser)
997
+
998
+ clear_parser = trust_subparsers.add_parser(
999
+ "clear",
1000
+ help="Remove all trusted skill directories",
1001
+ )
1002
+ add_json_output_arg(clear_parser)
1003
+
1004
+ return skills_parser
1005
+
1006
+
1007
+ def execute_skills_command(args: argparse.Namespace) -> None:
1008
+ """Execute skills subcommands based on parsed arguments."""
1009
+ if args.skills_command == "trust":
1010
+ _trust(args)
1011
+ return
1012
+
1013
+ agent = getattr(args, "agent", "opscloud") or "opscloud"
1014
+ is_valid, error_msg = _validate_name(agent)
1015
+ if not is_valid:
1016
+ console.print(f"[bold red]Error:[/bold red] Invalid agent name: {error_msg}")
1017
+ raise SystemExit(1)
1018
+
1019
+ output_format: OutputFormat = (
1020
+ "json" if getattr(args, "output_format", "text") == "json" else "text"
1021
+ )
1022
+
1023
+ if args.skills_command in {"list", "ls"}:
1024
+ _list(agent=agent, project=getattr(args, "project", False), output_format=output_format)
1025
+ elif args.skills_command == "create":
1026
+ _create(
1027
+ args.name,
1028
+ agent=agent,
1029
+ project=getattr(args, "project", False),
1030
+ output_format=output_format,
1031
+ )
1032
+ elif args.skills_command == "info":
1033
+ _info(
1034
+ args.name,
1035
+ agent=agent,
1036
+ project=getattr(args, "project", False),
1037
+ output_format=output_format,
1038
+ )
1039
+ elif args.skills_command in {"delete", "rm"}:
1040
+ _delete(
1041
+ args.name,
1042
+ agent=agent,
1043
+ project=getattr(args, "project", False),
1044
+ force=getattr(args, "force", False),
1045
+ dry_run=getattr(args, "dry_run", False),
1046
+ output_format=output_format,
1047
+ )
1048
+ else:
1049
+ from opscloud.ui import ui_help
1050
+
1051
+ ui_help.show_skills_help()
1052
+
1053
+
1054
+ # Backwards compatibility API
1055
+ def list_skills_command() -> list[dict[str, Any]]:
1056
+ """List all discovered skills and their trust status."""
1057
+ from opscloud.skills.load import list_skills
1058
+ from opscloud.skills.trust import SkillTrustStore
1059
+
1060
+ discovered = list_skills(project_root=settings.project_root)
1061
+ store = SkillTrustStore()
1062
+
1063
+ results = []
1064
+ for skill in discovered:
1065
+ path_str = skill.get("path", "")
1066
+ name_str = skill.get("name", "")
1067
+ source_str = skill.get("source", "")
1068
+ path = Path(path_str)
1069
+ trusted = store.is_trusted(name_str, path)
1070
+ results.append(
1071
+ {
1072
+ "name": name_str,
1073
+ "description": skill.get("description", ""),
1074
+ "source": source_str,
1075
+ "plugin_id": skill.get("plugin_id"),
1076
+ "path": str(path),
1077
+ "trusted": "Yes" if trusted else "No",
1078
+ }
1079
+ )
1080
+ return results
1081
+
1082
+
1083
+ def trust_skill_command(name: str, path_str: str) -> bool:
1084
+ """Trust a skill directory."""
1085
+ from opscloud.skills.trust import trust_skill_dir
1086
+
1087
+ path = Path(path_str).resolve()
1088
+ if not path.exists():
1089
+ return False
1090
+ return trust_skill_dir(path)
1091
+
1092
+
1093
+ __all__ = [
1094
+ "MAX_SKILL_NAME_LENGTH",
1095
+ "execute_skills_command",
1096
+ "list_skills_command",
1097
+ "setup_skills_parser",
1098
+ "trust_skill_command",
1099
+ ]