codepp 0.0.437__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.
- code_puppy/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
"""Interactive TUI for managing agent skills.
|
|
2
|
+
|
|
3
|
+
Launch with /skills to browse, enable, disable, and configure skills.
|
|
4
|
+
Built with prompt_toolkit for proper interactive split-panel interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from prompt_toolkit.application import Application
|
|
14
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
15
|
+
from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
|
|
16
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
17
|
+
from prompt_toolkit.widgets import Frame
|
|
18
|
+
|
|
19
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
20
|
+
from code_puppy.plugins.agent_skills.config import (
|
|
21
|
+
add_skill_directory,
|
|
22
|
+
get_disabled_skills,
|
|
23
|
+
get_skill_directories,
|
|
24
|
+
get_skills_enabled,
|
|
25
|
+
remove_skill_directory,
|
|
26
|
+
set_skill_disabled,
|
|
27
|
+
set_skills_enabled,
|
|
28
|
+
)
|
|
29
|
+
from code_puppy.plugins.agent_skills.discovery import (
|
|
30
|
+
SkillInfo,
|
|
31
|
+
discover_skills,
|
|
32
|
+
refresh_skill_cache,
|
|
33
|
+
)
|
|
34
|
+
from code_puppy.plugins.agent_skills.metadata import (
|
|
35
|
+
SkillMetadata,
|
|
36
|
+
get_skill_resources,
|
|
37
|
+
parse_skill_metadata,
|
|
38
|
+
)
|
|
39
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
40
|
+
|
|
41
|
+
PAGE_SIZE = 15 # Items per page
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SkillsMenu:
|
|
45
|
+
"""Interactive TUI for managing agent skills."""
|
|
46
|
+
|
|
47
|
+
def __init__(self):
|
|
48
|
+
"""Initialize the skills menu."""
|
|
49
|
+
self.skills: List[SkillInfo] = []
|
|
50
|
+
self.disabled_skills: List[str] = []
|
|
51
|
+
self.skill_directories: List[Path] = []
|
|
52
|
+
self.skills_enabled = False
|
|
53
|
+
|
|
54
|
+
# State management
|
|
55
|
+
self.selected_idx = 0
|
|
56
|
+
self.current_page = 0
|
|
57
|
+
self.result = None
|
|
58
|
+
|
|
59
|
+
# UI controls (set during run)
|
|
60
|
+
self.menu_control: Optional[FormattedTextControl] = None
|
|
61
|
+
self.preview_control: Optional[FormattedTextControl] = None
|
|
62
|
+
|
|
63
|
+
# Initialize data
|
|
64
|
+
self._refresh_data()
|
|
65
|
+
|
|
66
|
+
def _refresh_data(self) -> None:
|
|
67
|
+
"""Refresh skills data from disk."""
|
|
68
|
+
try:
|
|
69
|
+
self.skills = discover_skills()
|
|
70
|
+
self.disabled_skills = get_disabled_skills()
|
|
71
|
+
self.skill_directories = get_skill_directories()
|
|
72
|
+
self.skills_enabled = get_skills_enabled()
|
|
73
|
+
except Exception as e:
|
|
74
|
+
emit_error(f"Failed to refresh skills data: {e}")
|
|
75
|
+
|
|
76
|
+
def _get_current_skill(self) -> Optional[SkillInfo]:
|
|
77
|
+
"""Get the currently selected skill."""
|
|
78
|
+
if 0 <= self.selected_idx < len(self.skills):
|
|
79
|
+
return self.skills[self.selected_idx]
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
def _get_skill_metadata(self, skill: SkillInfo) -> Optional[SkillMetadata]:
|
|
83
|
+
"""Get metadata for a skill."""
|
|
84
|
+
try:
|
|
85
|
+
return parse_skill_metadata(skill.path)
|
|
86
|
+
except Exception:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def _is_skill_disabled(self, skill: SkillInfo) -> bool:
|
|
90
|
+
"""Check if a skill is disabled."""
|
|
91
|
+
metadata = self._get_skill_metadata(skill)
|
|
92
|
+
if metadata:
|
|
93
|
+
return metadata.name in self.disabled_skills
|
|
94
|
+
return skill.name in self.disabled_skills
|
|
95
|
+
|
|
96
|
+
def _toggle_current_skill(self) -> None:
|
|
97
|
+
"""Toggle the enabled/disabled state of the current skill."""
|
|
98
|
+
skill = self._get_current_skill()
|
|
99
|
+
if not skill:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
metadata = self._get_skill_metadata(skill)
|
|
103
|
+
skill_name = metadata.name if metadata else skill.name
|
|
104
|
+
|
|
105
|
+
is_disabled = skill_name in self.disabled_skills
|
|
106
|
+
set_skill_disabled(skill_name, not is_disabled)
|
|
107
|
+
refresh_skill_cache()
|
|
108
|
+
self._refresh_data()
|
|
109
|
+
self.update_display()
|
|
110
|
+
|
|
111
|
+
def _render_skill_list(self) -> List:
|
|
112
|
+
"""Render the skill list panel."""
|
|
113
|
+
lines = []
|
|
114
|
+
|
|
115
|
+
# Header with status
|
|
116
|
+
status_color = "fg:ansigreen" if self.skills_enabled else "fg:ansired"
|
|
117
|
+
status_text = "ENABLED" if self.skills_enabled else "DISABLED"
|
|
118
|
+
lines.append((status_color, f" Skills: {status_text}"))
|
|
119
|
+
lines.append(("", "\n\n"))
|
|
120
|
+
|
|
121
|
+
if not self.skills:
|
|
122
|
+
lines.append(("fg:ansiyellow", " No skills found."))
|
|
123
|
+
lines.append(("", "\n"))
|
|
124
|
+
lines.append(("fg:ansibrightblack", " Create skills in:"))
|
|
125
|
+
lines.append(("", "\n"))
|
|
126
|
+
lines.append(("fg:ansibrightblack", " ~/.code_puppy/skills/"))
|
|
127
|
+
lines.append(("", "\n"))
|
|
128
|
+
lines.append(("fg:ansibrightblack", " ./skills/"))
|
|
129
|
+
lines.append(("", "\n\n"))
|
|
130
|
+
self._render_navigation_hints(lines)
|
|
131
|
+
return lines
|
|
132
|
+
|
|
133
|
+
# Calculate pagination
|
|
134
|
+
total_pages = (len(self.skills) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
135
|
+
start_idx = self.current_page * PAGE_SIZE
|
|
136
|
+
end_idx = min(start_idx + PAGE_SIZE, len(self.skills))
|
|
137
|
+
|
|
138
|
+
# Render skills
|
|
139
|
+
for i in range(start_idx, end_idx):
|
|
140
|
+
skill = self.skills[i]
|
|
141
|
+
is_selected = i == self.selected_idx
|
|
142
|
+
is_disabled = self._is_skill_disabled(skill)
|
|
143
|
+
|
|
144
|
+
# Status icon
|
|
145
|
+
status_icon = "✗" if is_disabled else "✓"
|
|
146
|
+
status_style = "fg:ansired" if is_disabled else "fg:ansigreen"
|
|
147
|
+
|
|
148
|
+
# Get skill name from metadata if available
|
|
149
|
+
metadata = self._get_skill_metadata(skill)
|
|
150
|
+
display_name = metadata.name if metadata else skill.name
|
|
151
|
+
|
|
152
|
+
# Format line
|
|
153
|
+
prefix = " > " if is_selected else " "
|
|
154
|
+
|
|
155
|
+
if is_selected:
|
|
156
|
+
lines.append(("bold", prefix))
|
|
157
|
+
lines.append((status_style + " bold", status_icon))
|
|
158
|
+
lines.append(("bold", f" {display_name}"))
|
|
159
|
+
else:
|
|
160
|
+
lines.append(("", prefix))
|
|
161
|
+
lines.append((status_style, status_icon))
|
|
162
|
+
lines.append(("fg:ansibrightblack", f" {display_name}"))
|
|
163
|
+
|
|
164
|
+
lines.append(("", "\n"))
|
|
165
|
+
|
|
166
|
+
# Pagination info
|
|
167
|
+
lines.append(("", "\n"))
|
|
168
|
+
lines.append(
|
|
169
|
+
("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
|
|
170
|
+
)
|
|
171
|
+
lines.append(("", "\n"))
|
|
172
|
+
|
|
173
|
+
self._render_navigation_hints(lines)
|
|
174
|
+
return lines
|
|
175
|
+
|
|
176
|
+
def _render_navigation_hints(self, lines: List) -> None:
|
|
177
|
+
"""Render navigation hints at the bottom."""
|
|
178
|
+
lines.append(("", "\n"))
|
|
179
|
+
lines.append(("fg:ansibrightblack", " ↑/↓ or j/k "))
|
|
180
|
+
lines.append(("", "Navigate "))
|
|
181
|
+
lines.append(("fg:ansibrightblack", "←/→ "))
|
|
182
|
+
lines.append(("", "Page\n"))
|
|
183
|
+
lines.append(("fg:ansigreen", " Enter "))
|
|
184
|
+
lines.append(("", "Toggle "))
|
|
185
|
+
lines.append(("fg:ansicyan", " t "))
|
|
186
|
+
lines.append(("", "Toggle System\n"))
|
|
187
|
+
lines.append(("fg:ansimagenta", " Ctrl+A "))
|
|
188
|
+
lines.append(("", "Add Dir "))
|
|
189
|
+
lines.append(("fg:ansiyellow", " Ctrl+D "))
|
|
190
|
+
lines.append(("", "Show Dirs\n"))
|
|
191
|
+
lines.append(("fg:ansimagenta", " i "))
|
|
192
|
+
lines.append(("", "Install from catalog\n"))
|
|
193
|
+
lines.append(("fg:ansiyellow", " r "))
|
|
194
|
+
lines.append(("", "Refresh "))
|
|
195
|
+
lines.append(("fg:ansired", " q "))
|
|
196
|
+
lines.append(("", "Exit"))
|
|
197
|
+
|
|
198
|
+
def _render_skill_details(self) -> List:
|
|
199
|
+
"""Render the skill details panel."""
|
|
200
|
+
lines = []
|
|
201
|
+
|
|
202
|
+
lines.append(("dim cyan", " SKILL DETAILS"))
|
|
203
|
+
lines.append(("", "\n\n"))
|
|
204
|
+
|
|
205
|
+
skill = self._get_current_skill()
|
|
206
|
+
if not skill:
|
|
207
|
+
lines.append(("fg:ansiyellow", " No skill selected."))
|
|
208
|
+
lines.append(("", "\n\n"))
|
|
209
|
+
lines.append(("fg:ansibrightblack", " Select a skill from the list"))
|
|
210
|
+
lines.append(("", "\n"))
|
|
211
|
+
lines.append(("fg:ansibrightblack", " to view its details."))
|
|
212
|
+
return lines
|
|
213
|
+
|
|
214
|
+
metadata = self._get_skill_metadata(skill)
|
|
215
|
+
is_disabled = self._is_skill_disabled(skill)
|
|
216
|
+
|
|
217
|
+
# Status
|
|
218
|
+
status_text = "Disabled" if is_disabled else "Enabled"
|
|
219
|
+
status_style = "fg:ansired bold" if is_disabled else "fg:ansigreen bold"
|
|
220
|
+
lines.append(("bold", " Status: "))
|
|
221
|
+
lines.append((status_style, status_text))
|
|
222
|
+
lines.append(("", "\n\n"))
|
|
223
|
+
|
|
224
|
+
if metadata:
|
|
225
|
+
# Name
|
|
226
|
+
lines.append(("bold", f" {metadata.name}"))
|
|
227
|
+
lines.append(("", "\n\n"))
|
|
228
|
+
|
|
229
|
+
# Description
|
|
230
|
+
if metadata.description:
|
|
231
|
+
lines.append(("bold", " Description:"))
|
|
232
|
+
lines.append(("", "\n"))
|
|
233
|
+
# Wrap description
|
|
234
|
+
desc = metadata.description
|
|
235
|
+
wrapped = self._wrap_text(desc, 50)
|
|
236
|
+
for line in wrapped:
|
|
237
|
+
lines.append(("fg:ansibrightblack", f" {line}"))
|
|
238
|
+
lines.append(("", "\n"))
|
|
239
|
+
lines.append(("", "\n"))
|
|
240
|
+
|
|
241
|
+
# Tags
|
|
242
|
+
if metadata.tags:
|
|
243
|
+
lines.append(("bold", " Tags:"))
|
|
244
|
+
lines.append(("", "\n"))
|
|
245
|
+
tags_str = ", ".join(metadata.tags)
|
|
246
|
+
lines.append(("fg:ansicyan", f" {tags_str}"))
|
|
247
|
+
lines.append(("", "\n\n"))
|
|
248
|
+
|
|
249
|
+
# Resources
|
|
250
|
+
resources = get_skill_resources(metadata.path)
|
|
251
|
+
if resources:
|
|
252
|
+
lines.append(("bold", " Resources:"))
|
|
253
|
+
lines.append(("", "\n"))
|
|
254
|
+
for resource in resources[:5]: # Show first 5
|
|
255
|
+
resource_name = getattr(resource, "name", str(resource))
|
|
256
|
+
lines.append(("fg:ansiyellow", f" • {resource_name}"))
|
|
257
|
+
lines.append(("", "\n"))
|
|
258
|
+
if len(resources) > 5:
|
|
259
|
+
lines.append(
|
|
260
|
+
("fg:ansibrightblack", f" ... and {len(resources) - 5} more")
|
|
261
|
+
)
|
|
262
|
+
lines.append(("", "\n"))
|
|
263
|
+
lines.append(("", "\n"))
|
|
264
|
+
|
|
265
|
+
else:
|
|
266
|
+
# No metadata available
|
|
267
|
+
lines.append(("bold", f" {skill.name}"))
|
|
268
|
+
lines.append(("", "\n\n"))
|
|
269
|
+
lines.append(("fg:ansiyellow", " No metadata available"))
|
|
270
|
+
lines.append(("", "\n"))
|
|
271
|
+
lines.append(("fg:ansibrightblack", " Add a SKILL.md with frontmatter to"))
|
|
272
|
+
lines.append(("", "\n"))
|
|
273
|
+
lines.append(
|
|
274
|
+
("fg:ansibrightblack", " define name, description, and tags.")
|
|
275
|
+
)
|
|
276
|
+
lines.append(("", "\n\n"))
|
|
277
|
+
|
|
278
|
+
# Path
|
|
279
|
+
lines.append(("bold", " Path:"))
|
|
280
|
+
lines.append(("", "\n"))
|
|
281
|
+
path_str = str(skill.path)
|
|
282
|
+
if len(path_str) > 45:
|
|
283
|
+
path_str = "..." + path_str[-42:]
|
|
284
|
+
lines.append(("fg:ansibrightblack", f" {path_str}"))
|
|
285
|
+
lines.append(("", "\n"))
|
|
286
|
+
|
|
287
|
+
return lines
|
|
288
|
+
|
|
289
|
+
def _wrap_text(self, text: str, width: int) -> List[str]:
|
|
290
|
+
"""Wrap text to specified width."""
|
|
291
|
+
words = text.split()
|
|
292
|
+
lines = []
|
|
293
|
+
current_line = []
|
|
294
|
+
current_length = 0
|
|
295
|
+
|
|
296
|
+
for word in words:
|
|
297
|
+
if current_length + len(word) + 1 <= width:
|
|
298
|
+
current_line.append(word)
|
|
299
|
+
current_length += len(word) + 1
|
|
300
|
+
else:
|
|
301
|
+
if current_line:
|
|
302
|
+
lines.append(" ".join(current_line))
|
|
303
|
+
current_line = [word]
|
|
304
|
+
current_length = len(word)
|
|
305
|
+
|
|
306
|
+
if current_line:
|
|
307
|
+
lines.append(" ".join(current_line))
|
|
308
|
+
|
|
309
|
+
return lines or [""]
|
|
310
|
+
|
|
311
|
+
def update_display(self) -> None:
|
|
312
|
+
"""Update the display based on current state."""
|
|
313
|
+
if self.menu_control:
|
|
314
|
+
self.menu_control.text = self._render_skill_list()
|
|
315
|
+
if self.preview_control:
|
|
316
|
+
self.preview_control.text = self._render_skill_details()
|
|
317
|
+
|
|
318
|
+
def run(self) -> bool:
|
|
319
|
+
"""Run the interactive skills browser.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
True if changes were made, False otherwise.
|
|
323
|
+
"""
|
|
324
|
+
# Reset per-run state
|
|
325
|
+
self.result = None
|
|
326
|
+
|
|
327
|
+
# Build UI
|
|
328
|
+
self.menu_control = FormattedTextControl(text="")
|
|
329
|
+
self.preview_control = FormattedTextControl(text="")
|
|
330
|
+
|
|
331
|
+
menu_window = Window(
|
|
332
|
+
content=self.menu_control, wrap_lines=True, width=Dimension(weight=35)
|
|
333
|
+
)
|
|
334
|
+
preview_window = Window(
|
|
335
|
+
content=self.preview_control, wrap_lines=True, width=Dimension(weight=65)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
menu_frame = Frame(menu_window, width=Dimension(weight=35), title="Skills")
|
|
339
|
+
preview_frame = Frame(
|
|
340
|
+
preview_window, width=Dimension(weight=65), title="Details"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
root_container = VSplit([menu_frame, preview_frame])
|
|
344
|
+
|
|
345
|
+
# Key bindings
|
|
346
|
+
kb = KeyBindings()
|
|
347
|
+
|
|
348
|
+
@kb.add("up")
|
|
349
|
+
@kb.add("c-p") # Ctrl+P
|
|
350
|
+
@kb.add("k")
|
|
351
|
+
def _(event):
|
|
352
|
+
if self.selected_idx > 0:
|
|
353
|
+
self.selected_idx -= 1
|
|
354
|
+
self.current_page = self.selected_idx // PAGE_SIZE
|
|
355
|
+
self.update_display()
|
|
356
|
+
|
|
357
|
+
@kb.add("down")
|
|
358
|
+
@kb.add("c-n") # Ctrl+N
|
|
359
|
+
@kb.add("j")
|
|
360
|
+
def _(event):
|
|
361
|
+
if self.selected_idx < len(self.skills) - 1:
|
|
362
|
+
self.selected_idx += 1
|
|
363
|
+
self.current_page = self.selected_idx // PAGE_SIZE
|
|
364
|
+
self.update_display()
|
|
365
|
+
|
|
366
|
+
@kb.add("left")
|
|
367
|
+
def _(event):
|
|
368
|
+
"""Previous page."""
|
|
369
|
+
if self.current_page > 0:
|
|
370
|
+
self.current_page -= 1
|
|
371
|
+
self.selected_idx = self.current_page * PAGE_SIZE
|
|
372
|
+
self.update_display()
|
|
373
|
+
|
|
374
|
+
@kb.add("right")
|
|
375
|
+
def _(event):
|
|
376
|
+
"""Next page."""
|
|
377
|
+
total_pages = (len(self.skills) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
378
|
+
if self.current_page < total_pages - 1:
|
|
379
|
+
self.current_page += 1
|
|
380
|
+
self.selected_idx = self.current_page * PAGE_SIZE
|
|
381
|
+
self.update_display()
|
|
382
|
+
|
|
383
|
+
@kb.add("enter")
|
|
384
|
+
def _(event):
|
|
385
|
+
"""Toggle skill enabled/disabled."""
|
|
386
|
+
self._toggle_current_skill()
|
|
387
|
+
self.result = "changed"
|
|
388
|
+
|
|
389
|
+
@kb.add("t")
|
|
390
|
+
def _(event):
|
|
391
|
+
"""Toggle skills system on/off."""
|
|
392
|
+
new_state = not self.skills_enabled
|
|
393
|
+
set_skills_enabled(new_state)
|
|
394
|
+
self.skills_enabled = new_state
|
|
395
|
+
self.result = "changed"
|
|
396
|
+
self.update_display()
|
|
397
|
+
|
|
398
|
+
@kb.add("r")
|
|
399
|
+
def _(event):
|
|
400
|
+
"""Refresh skills."""
|
|
401
|
+
refresh_skill_cache()
|
|
402
|
+
self._refresh_data()
|
|
403
|
+
self.update_display()
|
|
404
|
+
|
|
405
|
+
@kb.add("c-a")
|
|
406
|
+
def _(event):
|
|
407
|
+
"""Add a skill directory."""
|
|
408
|
+
self.result = "add_directory"
|
|
409
|
+
event.app.exit()
|
|
410
|
+
|
|
411
|
+
@kb.add("c-d")
|
|
412
|
+
def _(event):
|
|
413
|
+
"""Show/manage directories."""
|
|
414
|
+
self.result = "show_directories"
|
|
415
|
+
event.app.exit()
|
|
416
|
+
|
|
417
|
+
@kb.add("i")
|
|
418
|
+
def _(event):
|
|
419
|
+
"""Install skills from catalog."""
|
|
420
|
+
self.result = "install"
|
|
421
|
+
event.app.exit()
|
|
422
|
+
|
|
423
|
+
@kb.add("q")
|
|
424
|
+
@kb.add("escape")
|
|
425
|
+
def _(event):
|
|
426
|
+
self.result = "quit"
|
|
427
|
+
event.app.exit()
|
|
428
|
+
|
|
429
|
+
@kb.add("c-c")
|
|
430
|
+
def _(event):
|
|
431
|
+
self.result = "quit"
|
|
432
|
+
event.app.exit()
|
|
433
|
+
|
|
434
|
+
layout = Layout(root_container)
|
|
435
|
+
app = Application(
|
|
436
|
+
layout=layout,
|
|
437
|
+
key_bindings=kb,
|
|
438
|
+
full_screen=False,
|
|
439
|
+
mouse_support=False,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
set_awaiting_user_input(True)
|
|
443
|
+
|
|
444
|
+
# Enter alternate screen buffer
|
|
445
|
+
sys.stdout.write("\033[?1049h") # Enter alternate buffer
|
|
446
|
+
sys.stdout.write("\033[2J\033[H") # Clear and home
|
|
447
|
+
sys.stdout.flush()
|
|
448
|
+
time.sleep(0.05)
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
# Initial display
|
|
452
|
+
self.update_display()
|
|
453
|
+
|
|
454
|
+
# Clear the buffer
|
|
455
|
+
sys.stdout.write("\033[2J\033[H")
|
|
456
|
+
sys.stdout.flush()
|
|
457
|
+
|
|
458
|
+
# Run application in a background thread to avoid event loop conflicts
|
|
459
|
+
app.run(in_thread=True)
|
|
460
|
+
|
|
461
|
+
finally:
|
|
462
|
+
# Exit alternate screen buffer
|
|
463
|
+
sys.stdout.write("\033[?1049l")
|
|
464
|
+
sys.stdout.flush()
|
|
465
|
+
|
|
466
|
+
# Flush any buffered input to prevent stale keypresses
|
|
467
|
+
try:
|
|
468
|
+
import termios
|
|
469
|
+
|
|
470
|
+
termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
|
|
471
|
+
except Exception:
|
|
472
|
+
pass # ImportError on Windows, termios.error, or not a tty
|
|
473
|
+
|
|
474
|
+
# Small delay to let terminal settle before any output
|
|
475
|
+
time.sleep(0.1)
|
|
476
|
+
set_awaiting_user_input(False)
|
|
477
|
+
|
|
478
|
+
return self.result
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _prompt_for_directory() -> Optional[str]:
|
|
482
|
+
"""Prompt user for a directory path to add."""
|
|
483
|
+
from code_puppy.tools.common import safe_input
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
print("\n" + "=" * 60)
|
|
487
|
+
print("ADD SKILL DIRECTORY")
|
|
488
|
+
print("=" * 60)
|
|
489
|
+
print("\nEnter the path to a directory containing skills.")
|
|
490
|
+
print("Examples:")
|
|
491
|
+
print(" ~/.claude/skills")
|
|
492
|
+
print(" /opt/shared-skills")
|
|
493
|
+
print(" ./my-project-skills")
|
|
494
|
+
print("\nPress Ctrl+C to cancel.\n")
|
|
495
|
+
|
|
496
|
+
path = safe_input("Directory path: ").strip()
|
|
497
|
+
if path:
|
|
498
|
+
# Expand ~ to home directory
|
|
499
|
+
expanded = os.path.expanduser(path)
|
|
500
|
+
return expanded
|
|
501
|
+
except (KeyboardInterrupt, EOFError):
|
|
502
|
+
print("\nCancelled.")
|
|
503
|
+
return None
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _show_directories_menu() -> Optional[str]:
|
|
507
|
+
"""Show current directories and allow removal."""
|
|
508
|
+
from code_puppy.tools.common import safe_input
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
dirs = get_skill_directories()
|
|
512
|
+
|
|
513
|
+
print("\n" + "=" * 60)
|
|
514
|
+
print("SKILL DIRECTORIES")
|
|
515
|
+
print("=" * 60)
|
|
516
|
+
print("\nCurrently configured directories:\n")
|
|
517
|
+
|
|
518
|
+
if not dirs:
|
|
519
|
+
print(" (no directories configured)")
|
|
520
|
+
else:
|
|
521
|
+
for i, d in enumerate(dirs, 1):
|
|
522
|
+
exists = os.path.isdir(os.path.expanduser(d))
|
|
523
|
+
status = "✓" if exists else "✗ (not found)"
|
|
524
|
+
print(f" {i}. {d} {status}")
|
|
525
|
+
|
|
526
|
+
print("\nOptions:")
|
|
527
|
+
print(" Enter a number to remove that directory")
|
|
528
|
+
print(" Press Enter or Ctrl+C to go back\n")
|
|
529
|
+
|
|
530
|
+
choice = safe_input("Choice: ").strip()
|
|
531
|
+
if choice and choice.isdigit():
|
|
532
|
+
idx = int(choice) - 1
|
|
533
|
+
if 0 <= idx < len(dirs):
|
|
534
|
+
dir_to_remove = dirs[idx]
|
|
535
|
+
confirm = (
|
|
536
|
+
safe_input(f"Remove '{dir_to_remove}'? (y/N): ").strip().lower()
|
|
537
|
+
)
|
|
538
|
+
if confirm in ("y", "yes"):
|
|
539
|
+
remove_skill_directory(dir_to_remove)
|
|
540
|
+
print(f"Removed: {dir_to_remove}")
|
|
541
|
+
return "changed"
|
|
542
|
+
except (KeyboardInterrupt, EOFError):
|
|
543
|
+
print("\nCancelled.")
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def show_skills_menu() -> bool:
|
|
548
|
+
"""Launch the interactive skills TUI menu.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
True if changes were made, False otherwise.
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
changes_made = False
|
|
555
|
+
|
|
556
|
+
while True:
|
|
557
|
+
menu = SkillsMenu()
|
|
558
|
+
result = menu.run()
|
|
559
|
+
|
|
560
|
+
if result == "add_directory":
|
|
561
|
+
# Prompt for directory to add
|
|
562
|
+
new_dir = _prompt_for_directory()
|
|
563
|
+
if new_dir:
|
|
564
|
+
if add_skill_directory(new_dir):
|
|
565
|
+
emit_success(f"Added skill directory: {new_dir}")
|
|
566
|
+
changes_made = True
|
|
567
|
+
else:
|
|
568
|
+
emit_warning(f"Directory already configured: {new_dir}")
|
|
569
|
+
# Re-run the menu
|
|
570
|
+
continue
|
|
571
|
+
|
|
572
|
+
elif result == "show_directories":
|
|
573
|
+
# Show directories management
|
|
574
|
+
dir_result = _show_directories_menu()
|
|
575
|
+
if dir_result == "changed":
|
|
576
|
+
changes_made = True
|
|
577
|
+
# Re-run the menu
|
|
578
|
+
continue
|
|
579
|
+
|
|
580
|
+
elif result == "install":
|
|
581
|
+
from code_puppy.plugins.agent_skills.skills_install_menu import (
|
|
582
|
+
run_skills_install_menu,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
install_result = run_skills_install_menu()
|
|
586
|
+
if install_result:
|
|
587
|
+
changes_made = True
|
|
588
|
+
continue # Re-run the skills menu after install
|
|
589
|
+
|
|
590
|
+
elif result == "changed":
|
|
591
|
+
changes_made = True
|
|
592
|
+
break
|
|
593
|
+
elif result == "quit":
|
|
594
|
+
break
|
|
595
|
+
else:
|
|
596
|
+
# User quit or no-op
|
|
597
|
+
break
|
|
598
|
+
|
|
599
|
+
return changes_made
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def list_skills() -> bool:
|
|
603
|
+
"""List all discovered skills in a simple format.
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
True if successful, False otherwise.
|
|
607
|
+
"""
|
|
608
|
+
try:
|
|
609
|
+
skills = discover_skills()
|
|
610
|
+
disabled_skills = get_disabled_skills()
|
|
611
|
+
|
|
612
|
+
if not skills:
|
|
613
|
+
emit_info(
|
|
614
|
+
"No skills found. Create skills in ~/.code_puppy/skills/ or ./skills/"
|
|
615
|
+
)
|
|
616
|
+
return True
|
|
617
|
+
|
|
618
|
+
emit_info(f"\nFound {len(skills)} skill(s):\n")
|
|
619
|
+
|
|
620
|
+
for skill in skills:
|
|
621
|
+
metadata = parse_skill_metadata(skill.path)
|
|
622
|
+
if metadata:
|
|
623
|
+
is_disabled = metadata.name in disabled_skills
|
|
624
|
+
status = "enabled" if not is_disabled else "disabled"
|
|
625
|
+
emit_info(f" [{status}] {metadata.name}")
|
|
626
|
+
if metadata.description:
|
|
627
|
+
emit_info(f" {metadata.description}")
|
|
628
|
+
resources = get_skill_resources(metadata.path)
|
|
629
|
+
if resources:
|
|
630
|
+
emit_info(f" Resources: {len(resources)}")
|
|
631
|
+
else:
|
|
632
|
+
is_disabled = skill.name in disabled_skills
|
|
633
|
+
status = "enabled" if not is_disabled else "disabled"
|
|
634
|
+
emit_info(f" [{status}] {skill.name} (no metadata)")
|
|
635
|
+
|
|
636
|
+
return True
|
|
637
|
+
except Exception as e:
|
|
638
|
+
emit_error(f"Failed to list skills: {e}")
|
|
639
|
+
return False
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def handle_skills_command(args: list[str]) -> bool:
|
|
643
|
+
"""Handle skills subcommands from the CLI.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
args: List of command arguments (e.g., ['enable', 'my-skill'])
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
True if successful, False otherwise.
|
|
650
|
+
"""
|
|
651
|
+
if not args:
|
|
652
|
+
# Show interactive TUI
|
|
653
|
+
return show_skills_menu()
|
|
654
|
+
|
|
655
|
+
command = args[0].lower()
|
|
656
|
+
|
|
657
|
+
if command == "list":
|
|
658
|
+
return list_skills()
|
|
659
|
+
elif command == "enable":
|
|
660
|
+
if len(args) < 2:
|
|
661
|
+
emit_error("Usage: /skills enable <skill-name>")
|
|
662
|
+
return False
|
|
663
|
+
return _enable_skill(args[1])
|
|
664
|
+
elif command == "disable":
|
|
665
|
+
if len(args) < 2:
|
|
666
|
+
emit_error("Usage: /skills disable <skill-name>")
|
|
667
|
+
return False
|
|
668
|
+
return _disable_skill(args[1])
|
|
669
|
+
elif command == "toggle":
|
|
670
|
+
return _toggle_skills_integration()
|
|
671
|
+
elif command == "refresh":
|
|
672
|
+
return _refresh_skills()
|
|
673
|
+
elif command == "help":
|
|
674
|
+
_show_help()
|
|
675
|
+
return True
|
|
676
|
+
else:
|
|
677
|
+
emit_error(f"Unknown command: {command}")
|
|
678
|
+
emit_info("Use '/skills help' to see available commands.")
|
|
679
|
+
return False
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _enable_skill(skill_name: str) -> bool:
|
|
683
|
+
"""Enable a specific skill."""
|
|
684
|
+
try:
|
|
685
|
+
skills = discover_skills()
|
|
686
|
+
skill_names = [s.name for s in skills]
|
|
687
|
+
|
|
688
|
+
# Also check metadata names
|
|
689
|
+
for skill in skills:
|
|
690
|
+
metadata = parse_skill_metadata(skill.path)
|
|
691
|
+
if metadata:
|
|
692
|
+
skill_names.append(metadata.name)
|
|
693
|
+
|
|
694
|
+
if skill_name not in skill_names:
|
|
695
|
+
emit_error(f"Skill '{skill_name}' not found.")
|
|
696
|
+
return False
|
|
697
|
+
|
|
698
|
+
disabled = get_disabled_skills()
|
|
699
|
+
if skill_name not in disabled:
|
|
700
|
+
emit_info(f"Skill '{skill_name}' is already enabled.")
|
|
701
|
+
return True
|
|
702
|
+
|
|
703
|
+
set_skill_disabled(skill_name, False)
|
|
704
|
+
refresh_skill_cache()
|
|
705
|
+
emit_success(f"Skill '{skill_name}' has been enabled.")
|
|
706
|
+
return True
|
|
707
|
+
except Exception as e:
|
|
708
|
+
emit_error(f"Failed to enable skill '{skill_name}': {e}")
|
|
709
|
+
return False
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _disable_skill(skill_name: str) -> bool:
|
|
713
|
+
"""Disable a specific skill."""
|
|
714
|
+
try:
|
|
715
|
+
skills = discover_skills()
|
|
716
|
+
skill_names = [s.name for s in skills]
|
|
717
|
+
|
|
718
|
+
# Also check metadata names
|
|
719
|
+
for skill in skills:
|
|
720
|
+
metadata = parse_skill_metadata(skill.path)
|
|
721
|
+
if metadata:
|
|
722
|
+
skill_names.append(metadata.name)
|
|
723
|
+
|
|
724
|
+
if skill_name not in skill_names:
|
|
725
|
+
emit_error(f"Skill '{skill_name}' not found.")
|
|
726
|
+
return False
|
|
727
|
+
|
|
728
|
+
disabled = get_disabled_skills()
|
|
729
|
+
if skill_name in disabled:
|
|
730
|
+
emit_info(f"Skill '{skill_name}' is already disabled.")
|
|
731
|
+
return True
|
|
732
|
+
|
|
733
|
+
set_skill_disabled(skill_name, True)
|
|
734
|
+
refresh_skill_cache()
|
|
735
|
+
emit_success(f"Skill '{skill_name}' has been disabled.")
|
|
736
|
+
return True
|
|
737
|
+
except Exception as e:
|
|
738
|
+
emit_error(f"Failed to disable skill '{skill_name}': {e}")
|
|
739
|
+
return False
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _toggle_skills_integration() -> bool:
|
|
743
|
+
"""Toggle skills integration on/off."""
|
|
744
|
+
try:
|
|
745
|
+
current = get_skills_enabled()
|
|
746
|
+
new_state = not current
|
|
747
|
+
set_skills_enabled(new_state)
|
|
748
|
+
|
|
749
|
+
if new_state:
|
|
750
|
+
emit_success("Skills integration has been enabled.")
|
|
751
|
+
else:
|
|
752
|
+
emit_warning("Skills integration has been disabled.")
|
|
753
|
+
|
|
754
|
+
return True
|
|
755
|
+
except Exception as e:
|
|
756
|
+
emit_error(f"Failed to toggle skills integration: {e}")
|
|
757
|
+
return False
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _refresh_skills() -> bool:
|
|
761
|
+
"""Refresh the skill cache."""
|
|
762
|
+
try:
|
|
763
|
+
emit_info("Refreshing skill cache...")
|
|
764
|
+
refresh_skill_cache()
|
|
765
|
+
emit_success("Skill cache refreshed successfully.")
|
|
766
|
+
return True
|
|
767
|
+
except Exception as e:
|
|
768
|
+
emit_error(f"Failed to refresh skill cache: {e}")
|
|
769
|
+
return False
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _show_help() -> None:
|
|
773
|
+
"""Show help information."""
|
|
774
|
+
emit_info("Available commands:")
|
|
775
|
+
emit_info(" /skills - Show interactive TUI")
|
|
776
|
+
emit_info(" /skills list - List all skills")
|
|
777
|
+
emit_info(" /skills enable <name> - Enable a skill")
|
|
778
|
+
emit_info(" /skills disable <name> - Disable a skill")
|
|
779
|
+
emit_info(" /skills toggle - Toggle skills integration")
|
|
780
|
+
emit_info(" /skills refresh - Refresh skill cache")
|
|
781
|
+
emit_info(" /skills help - Show this help")
|