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.
- opscloud/__init__.py +5 -0
- opscloud/__main__.py +6 -0
- opscloud/_constants.py +53 -0
- opscloud/_debug.py +145 -0
- opscloud/_debug_buffer.py +250 -0
- opscloud/_textual_patches.py +367 -0
- opscloud/_version.py +15 -0
- opscloud/agent/__init__.py +27 -0
- opscloud/agent/config.py +168 -0
- opscloud/agent/factory.py +1103 -0
- opscloud/approval_mode.py +44 -0
- opscloud/backend/__init__.py +49 -0
- opscloud/backend/composite.py +89 -0
- opscloud/backend/local.py +183 -0
- opscloud/backend/registry.py +79 -0
- opscloud/backend/sandbox_config.py +107 -0
- opscloud/backend/sandbox_factory.py +432 -0
- opscloud/built_in_skills/remember/SKILL.md +124 -0
- opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
- opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
- opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
- opscloud/cli/__init__.py +1 -0
- opscloud/cli/commands/__init__.py +15 -0
- opscloud/cli/commands/auth.py +45 -0
- opscloud/cli/commands/doctor.py +100 -0
- opscloud/cli/commands/mcp.py +137 -0
- opscloud/cli/commands/threads.py +51 -0
- opscloud/cli/interactive.py +154 -0
- opscloud/cli/main.py +903 -0
- opscloud/cli/non_interactive.py +425 -0
- opscloud/cli/server_manager.py +58 -0
- opscloud/commands/__init__.py +19 -0
- opscloud/commands/_base.py +110 -0
- opscloud/commands/_guards.py +52 -0
- opscloud/commands/_router.py +146 -0
- opscloud/commands/_types.py +36 -0
- opscloud/commands/core/__init__.py +47 -0
- opscloud/commands/core/auth.py +94 -0
- opscloud/commands/core/bug.py +46 -0
- opscloud/commands/core/clear.py +106 -0
- opscloud/commands/core/cloud.py +185 -0
- opscloud/commands/core/compact.py +128 -0
- opscloud/commands/core/config_cmd.py +120 -0
- opscloud/commands/core/context.py +220 -0
- opscloud/commands/core/cost.py +44 -0
- opscloud/commands/core/doctor.py +535 -0
- opscloud/commands/core/effort.py +108 -0
- opscloud/commands/core/exit_cmd.py +41 -0
- opscloud/commands/core/fast.py +148 -0
- opscloud/commands/core/help_cmd.py +94 -0
- opscloud/commands/core/mcp.py +134 -0
- opscloud/commands/core/model.py +109 -0
- opscloud/commands/core/permissions.py +196 -0
- opscloud/commands/core/plugins.py +92 -0
- opscloud/commands/core/pool.py +108 -0
- opscloud/commands/core/resume.py +97 -0
- opscloud/commands/core/skills.py +178 -0
- opscloud/commands/power/__init__.py +51 -0
- opscloud/commands/power/agents.py +98 -0
- opscloud/commands/power/btw.py +85 -0
- opscloud/commands/power/copy.py +97 -0
- opscloud/commands/power/goal.py +548 -0
- opscloud/commands/power/loop.py +295 -0
- opscloud/commands/power/memory.py +287 -0
- opscloud/commands/power/review.py +152 -0
- opscloud/commands/power/rubric.py +287 -0
- opscloud/commands/power/runtime.py +321 -0
- opscloud/commands/power/skill_creator.py +70 -0
- opscloud/commands/power/skill_invoke.py +165 -0
- opscloud/commands/power/tasks.py +209 -0
- opscloud/commands/power/trace.py +117 -0
- opscloud/commands/power/ui_toggles.py +135 -0
- opscloud/commands/power/version.py +52 -0
- opscloud/commands/registry.py +176 -0
- opscloud/config/__init__.py +114 -0
- opscloud/config/adapters/__init__.py +5 -0
- opscloud/config/adapters/postgres.py +776 -0
- opscloud/config/adapters/sqlite.py +769 -0
- opscloud/config/aws.py +248 -0
- opscloud/config/cloud_profiles.py +437 -0
- opscloud/config/env_vars.py +81 -0
- opscloud/config/langsmith.py +319 -0
- opscloud/config/manifest.py +817 -0
- opscloud/config/metadata.py +205 -0
- opscloud/config/paths.py +604 -0
- opscloud/config/plugins.py +130 -0
- opscloud/config/settings.py +834 -0
- opscloud/config/store.py +453 -0
- opscloud/config/store_factory.py +97 -0
- opscloud/config/toml_config.py +562 -0
- opscloud/editor.py +173 -0
- opscloud/exceptions.py +65 -0
- opscloud/file_ops.py +217 -0
- opscloud/hooks/__init__.py +139 -0
- opscloud/hooks/env.py +44 -0
- opscloud/hooks/interrupt.py +121 -0
- opscloud/hooks/models/__init__.py +109 -0
- opscloud/hooks/models/adapters.py +19 -0
- opscloud/hooks/models/config.py +79 -0
- opscloud/hooks/models/domain.py +434 -0
- opscloud/hooks/models/transport.py +37 -0
- opscloud/hooks/tools.py +231 -0
- opscloud/input.py +48 -0
- opscloud/integrations/__init__.py +80 -0
- opscloud/integrations/base.py +153 -0
- opscloud/integrations/content_filter.py +119 -0
- opscloud/integrations/event_bus.py +480 -0
- opscloud/integrations/hooks.py +238 -0
- opscloud/integrations/identity.py +181 -0
- opscloud/integrations/notifications.py +134 -0
- opscloud/integrations/stream_bridge.py +397 -0
- opscloud/integrations/thread_store.py +81 -0
- opscloud/json_types.py +20 -0
- opscloud/mcp/__init__.py +57 -0
- opscloud/mcp/config.py +203 -0
- opscloud/mcp/discovery.py +229 -0
- opscloud/mcp/mcp_info.py +109 -0
- opscloud/mcp/middleware.py +102 -0
- opscloud/mcp/preload.py +505 -0
- opscloud/mcp/raw_config.py +149 -0
- opscloud/mcp/semantic_profiler.py +305 -0
- opscloud/mcp/session_manager.py +727 -0
- opscloud/mcp/trust.py +105 -0
- opscloud/memory/__init__.py +27 -0
- opscloud/memory/branch.py +61 -0
- opscloud/memory/guard.py +19 -0
- opscloud/memory/onboarding.py +120 -0
- opscloud/memory/registry.py +134 -0
- opscloud/memory/store.py +223 -0
- opscloud/middleware/__init__.py +106 -0
- opscloud/middleware/_repository_bounds.py +459 -0
- opscloud/middleware/ask_user.py +152 -0
- opscloud/middleware/auto_mode.py +60 -0
- opscloud/middleware/auto_mode_hitl.py +2434 -0
- opscloud/middleware/compaction.py +566 -0
- opscloud/middleware/configurable_model.py +233 -0
- opscloud/middleware/cost_tracking.py +1021 -0
- opscloud/middleware/glm_stall_recovery.py +109 -0
- opscloud/middleware/goal_criteria.py +1234 -0
- opscloud/middleware/goal_state_notice.py +712 -0
- opscloud/middleware/goal_tools.py +407 -0
- opscloud/middleware/headless_mcp_guard.py +88 -0
- opscloud/middleware/jev_model_router.py +1018 -0
- opscloud/middleware/local_context.py +460 -0
- opscloud/middleware/mcp_context.py +151 -0
- opscloud/middleware/mcp_middleware.py +146 -0
- opscloud/middleware/memory_guard.py +393 -0
- opscloud/middleware/model_retry.py +205 -0
- opscloud/middleware/registry.py +137 -0
- opscloud/middleware/reliable_rubric.py +709 -0
- opscloud/middleware/resume_state.py +91 -0
- opscloud/middleware/server_hooks.py +1189 -0
- opscloud/middleware/shell_allow_list.py +90 -0
- opscloud/middleware/skills.py +382 -0
- opscloud/middleware/subagent_artifacts.py +84 -0
- opscloud/middleware/subagent_telemetry.py +261 -0
- opscloud/middleware/subagents.py +243 -0
- opscloud/middleware/tool_filter.py +384 -0
- opscloud/middleware/unified_system_message.py +62 -0
- opscloud/model/__init__.py +37 -0
- opscloud/model/config.py +2992 -0
- opscloud/model/factory.py +477 -0
- opscloud/model/pool.py +459 -0
- opscloud/model/reasoning.py +282 -0
- opscloud/offload.py +173 -0
- opscloud/output.py +64 -0
- opscloud/plugins/__init__.py +189 -0
- opscloud/plugins/_json.py +22 -0
- opscloud/plugins/adapters/__init__.py +13 -0
- opscloud/plugins/adapters/agents.py +112 -0
- opscloud/plugins/adapters/commands.py +190 -0
- opscloud/plugins/adapters/mcp.py +363 -0
- opscloud/plugins/adapters/skills.py +132 -0
- opscloud/plugins/commands_cli.py +541 -0
- opscloud/plugins/discovery.py +512 -0
- opscloud/plugins/manifest.py +335 -0
- opscloud/plugins/marketplace.py +893 -0
- opscloud/plugins/models.py +334 -0
- opscloud/plugins/project_plugins.py +317 -0
- opscloud/plugins/protocol.py +31 -0
- opscloud/plugins/store.py +905 -0
- opscloud/plugins/substitution.py +122 -0
- opscloud/project_utils.py +145 -0
- opscloud/prompts/__init__.py +37 -0
- opscloud/prompts/system.py +483 -0
- opscloud/prompts/templates/system_prompt.md +166 -0
- opscloud/prompts/types.py +54 -0
- opscloud/rubrics/__init__.py +42 -0
- opscloud/rubrics/evaluator.py +404 -0
- opscloud/rubrics/evidence_extractor.py +229 -0
- opscloud/rubrics/generator.py +239 -0
- opscloud/rubrics/jev_compiler.py +175 -0
- opscloud/rubrics/jev_grader.py +297 -0
- opscloud/rubrics/middleware.py +8 -0
- opscloud/schema/__init__.py +107 -0
- opscloud/schema/interrupts.py +981 -0
- opscloud/security/__init__.py +91 -0
- opscloud/security/approval_mode.py +528 -0
- opscloud/security/approval_mode_source.py +230 -0
- opscloud/security/cli_ast_evaluator.py +552 -0
- opscloud/security/jev_classifier.py +651 -0
- opscloud/security/shell_safety.py +340 -0
- opscloud/security/unicode_security.py +439 -0
- opscloud/security/url_validation.py +160 -0
- opscloud/server/__init__.py +13 -0
- opscloud/server/_server_config.py +158 -0
- opscloud/server/server.py +242 -0
- opscloud/server/server_graph.py +93 -0
- opscloud/skills/__init__.py +91 -0
- opscloud/skills/commands.py +1099 -0
- opscloud/skills/invocation.py +200 -0
- opscloud/skills/load.py +388 -0
- opscloud/skills/loader.py +23 -0
- opscloud/skills/merge.py +65 -0
- opscloud/skills/registry.py +263 -0
- opscloud/skills/sources.py +174 -0
- opscloud/skills/trust.py +417 -0
- opscloud/state/__init__.py +142 -0
- opscloud/state/base.py +154 -0
- opscloud/state/cloud_context.py +262 -0
- opscloud/state/goal_channels.py +124 -0
- opscloud/state/goal_state_limits.py +175 -0
- opscloud/state/resume_state.py +58 -0
- opscloud/state/service_context.py +281 -0
- opscloud/state/session.py +957 -0
- opscloud/state/state_migration.py +86 -0
- opscloud/subagents/__init__.py +30 -0
- opscloud/subagents/loader.py +331 -0
- opscloud/subagents/subagents_parser.py +117 -0
- opscloud/subagents/types.py +25 -0
- opscloud/tools/__init__.py +36 -0
- opscloud/tools/catalog.py +37 -0
- opscloud/tools/display.py +24 -0
- opscloud/tools/fetch_url.py +176 -0
- opscloud/tools/goal_tools.py +423 -0
- opscloud/tools/registry.py +105 -0
- opscloud/tools/thread.py +32 -0
- opscloud/tools/web_search.py +215 -0
- opscloud/ui/__init__.py +119 -0
- opscloud/ui/app.py +5400 -0
- opscloud/ui/app.tcss +479 -0
- opscloud/ui/clipboard.py +147 -0
- opscloud/ui/command_registry.py +482 -0
- opscloud/ui/message_store.py +152 -0
- opscloud/ui/permission_store.py +360 -0
- opscloud/ui/preferences.py +113 -0
- opscloud/ui/prompt_manager.py +96 -0
- opscloud/ui/remote_client.py +505 -0
- opscloud/ui/textual_adapter.py +1269 -0
- opscloud/ui/theme.py +728 -0
- opscloud/ui/ui_help.py +415 -0
- opscloud/ui/widgets/__init__.py +383 -0
- opscloud/ui/widgets/_ask_user_types.py +81 -0
- opscloud/ui/widgets/_copy_spans.py +52 -0
- opscloud/ui/widgets/_inline_prompt.py +290 -0
- opscloud/ui/widgets/_js_eval_display.py +139 -0
- opscloud/ui/widgets/_links.py +175 -0
- opscloud/ui/widgets/_tool_stream.py +308 -0
- opscloud/ui/widgets/agent_selector.py +297 -0
- opscloud/ui/widgets/approval.py +741 -0
- opscloud/ui/widgets/ask_user.py +539 -0
- opscloud/ui/widgets/auth.py +695 -0
- opscloud/ui/widgets/auth_manager.py +214 -0
- opscloud/ui/widgets/auto_mode_notice.py +146 -0
- opscloud/ui/widgets/autocomplete.py +709 -0
- opscloud/ui/widgets/chat_input.py +805 -0
- opscloud/ui/widgets/cloud_selector.py +528 -0
- opscloud/ui/widgets/config_manager.py +652 -0
- opscloud/ui/widgets/debug_console.py +1269 -0
- opscloud/ui/widgets/devops_renderers.py +184 -0
- opscloud/ui/widgets/diff.py +237 -0
- opscloud/ui/widgets/effort_selector.py +166 -0
- opscloud/ui/widgets/goal_review.py +612 -0
- opscloud/ui/widgets/goal_status.py +57 -0
- opscloud/ui/widgets/infra_panel.py +141 -0
- opscloud/ui/widgets/install_confirm.py +114 -0
- opscloud/ui/widgets/loading.py +170 -0
- opscloud/ui/widgets/mcp_viewer.py +1204 -0
- opscloud/ui/widgets/messages.py +4461 -0
- opscloud/ui/widgets/model_selector.py +863 -0
- opscloud/ui/widgets/notification_center.py +108 -0
- opscloud/ui/widgets/notification_settings.py +119 -0
- opscloud/ui/widgets/operation_card.py +106 -0
- opscloud/ui/widgets/permissions_manager.py +667 -0
- opscloud/ui/widgets/plugin_manager.py +1212 -0
- opscloud/ui/widgets/pool_selector.py +376 -0
- opscloud/ui/widgets/preamble.py +53 -0
- opscloud/ui/widgets/skills_viewer.py +432 -0
- opscloud/ui/widgets/status.py +1037 -0
- opscloud/ui/widgets/subagent_panel.py +927 -0
- opscloud/ui/widgets/theme_selector.py +232 -0
- opscloud/ui/widgets/thread_selector.py +358 -0
- opscloud/ui/widgets/toast.py +31 -0
- opscloud/ui/widgets/tool_display.py +325 -0
- opscloud/ui/widgets/tool_renderers.py +234 -0
- opscloud/ui/widgets/tool_widgets.py +278 -0
- opscloud/ui/widgets/welcome.py +421 -0
- opscloud/ui/widgets/welcome_popup.py +102 -0
- opscloud/utils/__init__.py +24 -0
- opscloud/utils/cost_estimation.py +169 -0
- opscloud/utils/git.py +390 -0
- opscloud/utils/logger.py +571 -0
- opscloud/utils/session_stats.py +115 -0
- opscloud/utils/startup_error.py +37 -0
- talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
- talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
- talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
- talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|