code-puppy 0.0.169__py3-none-any.whl → 0.0.366__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 +7 -1
- code_puppy/agents/__init__.py +8 -8
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +9 -2
- 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 +48 -9
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +146 -199
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -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_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 +1713 -1
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/json_agent.py +12 -1
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -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 +446 -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 +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +174 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +233 -627
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +1 -4
- 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 +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +16 -27
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +3 -3
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +17 -11
- code_puppy/command_line/mcp/start_all_command.py +22 -13
- code_puppy/command_line/mcp/start_command.py +50 -31
- code_puppy/command_line/mcp/status_command.py +6 -7
- code_puppy/command_line/mcp/stop_all_command.py +11 -8
- code_puppy/command_line/mcp/stop_command.py +11 -10
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +22 -18
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +89 -30
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +626 -75
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +1181 -51
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +220 -104
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -594
- code_puppy/{mcp → mcp_}/__init__.py +17 -0
- code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
- code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
- code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
- code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
- code_puppy/{mcp → mcp_}/dashboard.py +15 -6
- code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
- code_puppy/{mcp → mcp_}/managed_server.py +66 -39
- code_puppy/{mcp → mcp_}/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/{mcp → mcp_}/registry.py +6 -6
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +33 -5
- code_puppy/messaging/spinner/console_spinner.py +92 -52
- code_puppy/messaging/spinner/spinner_base.py +29 -0
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +686 -80
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +86 -104
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- 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 +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -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 +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -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 +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -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 +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -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 +523 -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/oauth_puppy_html.py +228 -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/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +10 -15
- code_puppy/session_storage.py +294 -0
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +52 -14
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +139 -6
- code_puppy/tools/agent_tools.py +548 -49
- 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 +316 -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 +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +941 -153
- code_puppy/tools/common.py +1146 -6
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +352 -266
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
- code_puppy/agent.py +0 -231
- code_puppy/agents/agent_orchestrator.json +0 -26
- code_puppy/agents/runtime_manager.py +0 -272
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/command_line/meta_command_handler.py +0 -153
- code_puppy/message_history_processor.py +0 -490
- code_puppy/messaging/spinner/textual_spinner.py +0 -101
- code_puppy/state_management.py +0 -200
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -986
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -550
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -182
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -15
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -290
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
- code_puppy-0.0.169.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
"""Command handlers for Code Puppy - CORE commands.
|
|
2
|
+
|
|
3
|
+
This module contains @register_command decorated handlers that are automatically
|
|
4
|
+
discovered by the command registry system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from code_puppy.command_line.agent_menu import interactive_agent_picker
|
|
10
|
+
from code_puppy.command_line.command_registry import register_command
|
|
11
|
+
from code_puppy.command_line.model_picker_completion import update_model_in_input
|
|
12
|
+
from code_puppy.command_line.motd import print_motd
|
|
13
|
+
from code_puppy.command_line.utils import make_directory_table
|
|
14
|
+
from code_puppy.config import finalize_autosave_session
|
|
15
|
+
from code_puppy.messaging import emit_error, emit_info
|
|
16
|
+
from code_puppy.tools.tools_content import tools_content
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Import get_commands_help from command_handler to avoid circular imports
|
|
20
|
+
# This will be defined in command_handler.py
|
|
21
|
+
def get_commands_help():
|
|
22
|
+
"""Lazy import to avoid circular dependency."""
|
|
23
|
+
from code_puppy.command_line.command_handler import get_commands_help as _gch
|
|
24
|
+
|
|
25
|
+
return _gch()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@register_command(
|
|
29
|
+
name="help",
|
|
30
|
+
description="Show this help message",
|
|
31
|
+
usage="/help, /h",
|
|
32
|
+
aliases=["h"],
|
|
33
|
+
category="core",
|
|
34
|
+
)
|
|
35
|
+
def handle_help_command(command: str) -> bool:
|
|
36
|
+
"""Show commands help."""
|
|
37
|
+
import uuid
|
|
38
|
+
|
|
39
|
+
from code_puppy.messaging import emit_info
|
|
40
|
+
|
|
41
|
+
group_id = str(uuid.uuid4())
|
|
42
|
+
help_text = get_commands_help()
|
|
43
|
+
emit_info(help_text, message_group_id=group_id)
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@register_command(
|
|
48
|
+
name="cd",
|
|
49
|
+
description="Change directory or show directories",
|
|
50
|
+
usage="/cd <dir>",
|
|
51
|
+
category="core",
|
|
52
|
+
)
|
|
53
|
+
def handle_cd_command(command: str) -> bool:
|
|
54
|
+
"""Change directory or list current directory."""
|
|
55
|
+
# Use shlex.split to handle quoted paths properly
|
|
56
|
+
import shlex
|
|
57
|
+
|
|
58
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
tokens = shlex.split(command)
|
|
62
|
+
except ValueError:
|
|
63
|
+
# Fallback to simple split if shlex fails
|
|
64
|
+
tokens = command.split()
|
|
65
|
+
if len(tokens) == 1:
|
|
66
|
+
try:
|
|
67
|
+
table = make_directory_table()
|
|
68
|
+
emit_info(table)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
emit_error(f"Error listing directory: {e}")
|
|
71
|
+
return True
|
|
72
|
+
elif len(tokens) == 2:
|
|
73
|
+
dirname = tokens[1]
|
|
74
|
+
target = os.path.expanduser(dirname)
|
|
75
|
+
if not os.path.isabs(target):
|
|
76
|
+
target = os.path.join(os.getcwd(), target)
|
|
77
|
+
if os.path.isdir(target):
|
|
78
|
+
os.chdir(target)
|
|
79
|
+
emit_success(f"Changed directory to: {target}")
|
|
80
|
+
else:
|
|
81
|
+
emit_error(f"Not a directory: {dirname}")
|
|
82
|
+
return True
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@register_command(
|
|
87
|
+
name="tools",
|
|
88
|
+
description="Show available tools and capabilities",
|
|
89
|
+
usage="/tools",
|
|
90
|
+
category="core",
|
|
91
|
+
)
|
|
92
|
+
def handle_tools_command(command: str) -> bool:
|
|
93
|
+
"""Display available tools."""
|
|
94
|
+
from rich.markdown import Markdown
|
|
95
|
+
|
|
96
|
+
from code_puppy.messaging import emit_info
|
|
97
|
+
|
|
98
|
+
markdown_content = Markdown(tools_content)
|
|
99
|
+
emit_info(markdown_content)
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@register_command(
|
|
104
|
+
name="motd",
|
|
105
|
+
description="Show the latest message of the day (MOTD)",
|
|
106
|
+
usage="/motd",
|
|
107
|
+
category="core",
|
|
108
|
+
)
|
|
109
|
+
def handle_motd_command(command: str) -> bool:
|
|
110
|
+
"""Show message of the day."""
|
|
111
|
+
try:
|
|
112
|
+
print_motd(force=True)
|
|
113
|
+
except Exception:
|
|
114
|
+
# Handle printing errors gracefully
|
|
115
|
+
pass
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@register_command(
|
|
120
|
+
name="paste",
|
|
121
|
+
description="Paste image from clipboard (same as F3, or Ctrl+V with image)",
|
|
122
|
+
usage="/paste, /clipboard, /cb",
|
|
123
|
+
aliases=["clipboard", "cb"],
|
|
124
|
+
category="core",
|
|
125
|
+
)
|
|
126
|
+
def handle_paste_command(command: str) -> bool:
|
|
127
|
+
"""Paste an image from the clipboard into the pending attachments."""
|
|
128
|
+
from code_puppy.command_line.clipboard import (
|
|
129
|
+
capture_clipboard_image_to_pending,
|
|
130
|
+
get_clipboard_manager,
|
|
131
|
+
has_image_in_clipboard,
|
|
132
|
+
)
|
|
133
|
+
from code_puppy.messaging import emit_info, emit_success, emit_warning
|
|
134
|
+
|
|
135
|
+
if not has_image_in_clipboard():
|
|
136
|
+
emit_warning("No image found in clipboard")
|
|
137
|
+
emit_info("Copy an image (screenshot, from browser, etc.) and try again")
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
placeholder = capture_clipboard_image_to_pending()
|
|
141
|
+
if placeholder:
|
|
142
|
+
manager = get_clipboard_manager()
|
|
143
|
+
count = manager.get_pending_count()
|
|
144
|
+
emit_success(f"📋 {placeholder}")
|
|
145
|
+
emit_info(f"Total pending clipboard images: {count}")
|
|
146
|
+
emit_info("Type your prompt and press Enter to send with the image(s)")
|
|
147
|
+
else:
|
|
148
|
+
emit_warning("Failed to capture clipboard image")
|
|
149
|
+
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@register_command(
|
|
154
|
+
name="tutorial",
|
|
155
|
+
description="Run the interactive tutorial wizard",
|
|
156
|
+
usage="/tutorial",
|
|
157
|
+
category="core",
|
|
158
|
+
)
|
|
159
|
+
def handle_tutorial_command(command: str) -> bool:
|
|
160
|
+
"""Run the interactive tutorial wizard.
|
|
161
|
+
|
|
162
|
+
Usage:
|
|
163
|
+
/tutorial - Run the tutorial (can be run anytime)
|
|
164
|
+
"""
|
|
165
|
+
import asyncio
|
|
166
|
+
import concurrent.futures
|
|
167
|
+
|
|
168
|
+
from code_puppy.command_line.onboarding_wizard import (
|
|
169
|
+
reset_onboarding,
|
|
170
|
+
run_onboarding_wizard,
|
|
171
|
+
)
|
|
172
|
+
from code_puppy.config import set_model_name
|
|
173
|
+
|
|
174
|
+
# Always reset so user can re-run the tutorial anytime
|
|
175
|
+
reset_onboarding()
|
|
176
|
+
|
|
177
|
+
# Run the async wizard in a thread pool (same pattern as agent picker)
|
|
178
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
179
|
+
future = executor.submit(lambda: asyncio.run(run_onboarding_wizard()))
|
|
180
|
+
result = future.result(timeout=300) # 5 min timeout
|
|
181
|
+
|
|
182
|
+
if result == "chatgpt":
|
|
183
|
+
emit_info("🔐 Starting ChatGPT OAuth flow...")
|
|
184
|
+
from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
|
|
185
|
+
|
|
186
|
+
run_oauth_flow()
|
|
187
|
+
set_model_name("chatgpt-gpt-5.2-codex")
|
|
188
|
+
elif result == "claude":
|
|
189
|
+
emit_info("🔐 Starting Claude Code OAuth flow...")
|
|
190
|
+
from code_puppy.plugins.claude_code_oauth.register_callbacks import (
|
|
191
|
+
_perform_authentication,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
_perform_authentication()
|
|
195
|
+
set_model_name("claude-code-claude-opus-4-5-20251101")
|
|
196
|
+
elif result == "completed":
|
|
197
|
+
emit_info("🎉 Tutorial complete! Happy coding!")
|
|
198
|
+
elif result == "skipped":
|
|
199
|
+
emit_info("⏭️ Tutorial skipped. Run /tutorial anytime!")
|
|
200
|
+
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@register_command(
|
|
205
|
+
name="exit",
|
|
206
|
+
description="Exit interactive mode",
|
|
207
|
+
usage="/exit, /quit",
|
|
208
|
+
aliases=["quit"],
|
|
209
|
+
category="core",
|
|
210
|
+
)
|
|
211
|
+
def handle_exit_command(command: str) -> bool:
|
|
212
|
+
"""Exit the interactive session."""
|
|
213
|
+
from code_puppy.messaging import emit_success
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
emit_success("Goodbye!")
|
|
217
|
+
except Exception:
|
|
218
|
+
# Handle emit errors gracefully
|
|
219
|
+
pass
|
|
220
|
+
# Signal to the main app that we want to exit
|
|
221
|
+
# The actual exit handling is done in main.py
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@register_command(
|
|
226
|
+
name="agent",
|
|
227
|
+
description="Switch to a different agent or show available agents",
|
|
228
|
+
usage="/agent <name>, /a <name>",
|
|
229
|
+
aliases=["a"],
|
|
230
|
+
category="core",
|
|
231
|
+
)
|
|
232
|
+
def handle_agent_command(command: str) -> bool:
|
|
233
|
+
"""Handle agent switching."""
|
|
234
|
+
from rich.text import Text
|
|
235
|
+
|
|
236
|
+
from code_puppy.agents import (
|
|
237
|
+
get_agent_descriptions,
|
|
238
|
+
get_available_agents,
|
|
239
|
+
get_current_agent,
|
|
240
|
+
set_current_agent,
|
|
241
|
+
)
|
|
242
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
243
|
+
|
|
244
|
+
tokens = command.split()
|
|
245
|
+
|
|
246
|
+
if len(tokens) == 1:
|
|
247
|
+
# Show interactive agent picker
|
|
248
|
+
try:
|
|
249
|
+
# Run the async picker using asyncio utilities
|
|
250
|
+
# Since we're called from an async context but this function is sync,
|
|
251
|
+
# we need to carefully schedule and wait for the coroutine
|
|
252
|
+
import asyncio
|
|
253
|
+
import concurrent.futures
|
|
254
|
+
import uuid
|
|
255
|
+
|
|
256
|
+
# Create a new event loop in a thread and run the picker there
|
|
257
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
258
|
+
future = executor.submit(
|
|
259
|
+
lambda: asyncio.run(interactive_agent_picker())
|
|
260
|
+
)
|
|
261
|
+
selected_agent = future.result(timeout=300) # 5 min timeout
|
|
262
|
+
|
|
263
|
+
if selected_agent:
|
|
264
|
+
current_agent = get_current_agent()
|
|
265
|
+
# Check if we're already using this agent
|
|
266
|
+
if current_agent.name == selected_agent:
|
|
267
|
+
group_id = str(uuid.uuid4())
|
|
268
|
+
emit_info(
|
|
269
|
+
f"Already using agent: {current_agent.display_name}",
|
|
270
|
+
message_group=group_id,
|
|
271
|
+
)
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
# Switch to the new agent
|
|
275
|
+
group_id = str(uuid.uuid4())
|
|
276
|
+
new_session_id = finalize_autosave_session()
|
|
277
|
+
if not set_current_agent(selected_agent):
|
|
278
|
+
emit_warning(
|
|
279
|
+
"Agent switch failed after autosave rotation. Your context was preserved.",
|
|
280
|
+
message_group=group_id,
|
|
281
|
+
)
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
new_agent = get_current_agent()
|
|
285
|
+
new_agent.reload_code_generation_agent()
|
|
286
|
+
emit_success(
|
|
287
|
+
f"Switched to agent: {new_agent.display_name}",
|
|
288
|
+
message_group=group_id,
|
|
289
|
+
)
|
|
290
|
+
emit_info(f"{new_agent.description}", message_group=group_id)
|
|
291
|
+
emit_info(
|
|
292
|
+
Text.from_markup(
|
|
293
|
+
f"[dim]Auto-save session rotated to: {new_session_id}[/dim]"
|
|
294
|
+
),
|
|
295
|
+
message_group=group_id,
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
emit_warning("Agent selection cancelled")
|
|
299
|
+
return True
|
|
300
|
+
except Exception as e:
|
|
301
|
+
# Fallback to old behavior if picker fails
|
|
302
|
+
import traceback
|
|
303
|
+
import uuid
|
|
304
|
+
|
|
305
|
+
emit_warning(f"Interactive picker failed: {e}")
|
|
306
|
+
emit_warning(f"Traceback: {traceback.format_exc()}")
|
|
307
|
+
|
|
308
|
+
# Show current agent and available agents
|
|
309
|
+
current_agent = get_current_agent()
|
|
310
|
+
available_agents = get_available_agents()
|
|
311
|
+
descriptions = get_agent_descriptions()
|
|
312
|
+
|
|
313
|
+
# Generate a group ID for all messages in this command
|
|
314
|
+
group_id = str(uuid.uuid4())
|
|
315
|
+
|
|
316
|
+
emit_info(
|
|
317
|
+
Text.from_markup(
|
|
318
|
+
f"[bold green]Current Agent:[/bold green] {current_agent.display_name}"
|
|
319
|
+
),
|
|
320
|
+
message_group=group_id,
|
|
321
|
+
)
|
|
322
|
+
emit_info(
|
|
323
|
+
Text.from_markup(f"[dim]{current_agent.description}[/dim]\n"),
|
|
324
|
+
message_group=group_id,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
emit_info(
|
|
328
|
+
Text.from_markup("[bold magenta]Available Agents:[/bold magenta]"),
|
|
329
|
+
message_group=group_id,
|
|
330
|
+
)
|
|
331
|
+
for name, display_name in available_agents.items():
|
|
332
|
+
description = descriptions.get(name, "No description")
|
|
333
|
+
current_marker = (
|
|
334
|
+
" [green]← current[/green]" if name == current_agent.name else ""
|
|
335
|
+
)
|
|
336
|
+
emit_info(
|
|
337
|
+
Text.from_markup(
|
|
338
|
+
f" [cyan]{name:<12}[/cyan] {display_name}{current_marker}"
|
|
339
|
+
),
|
|
340
|
+
message_group=group_id,
|
|
341
|
+
)
|
|
342
|
+
emit_info(f" {description}", message_group=group_id)
|
|
343
|
+
|
|
344
|
+
emit_info(
|
|
345
|
+
Text.from_markup("\n[yellow]Usage:[/yellow] /agent <agent-name>"),
|
|
346
|
+
message_group=group_id,
|
|
347
|
+
)
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
elif len(tokens) == 2:
|
|
351
|
+
agent_name = tokens[1].lower()
|
|
352
|
+
|
|
353
|
+
# Generate a group ID for all messages in this command
|
|
354
|
+
import uuid
|
|
355
|
+
|
|
356
|
+
group_id = str(uuid.uuid4())
|
|
357
|
+
available_agents = get_available_agents()
|
|
358
|
+
|
|
359
|
+
if agent_name not in available_agents:
|
|
360
|
+
emit_error(f"Agent '{agent_name}' not found", message_group=group_id)
|
|
361
|
+
emit_warning(
|
|
362
|
+
f"Available agents: {', '.join(available_agents.keys())}",
|
|
363
|
+
message_group=group_id,
|
|
364
|
+
)
|
|
365
|
+
return True
|
|
366
|
+
|
|
367
|
+
current_agent = get_current_agent()
|
|
368
|
+
if current_agent.name == agent_name:
|
|
369
|
+
emit_info(
|
|
370
|
+
f"Already using agent: {current_agent.display_name}",
|
|
371
|
+
message_group=group_id,
|
|
372
|
+
)
|
|
373
|
+
return True
|
|
374
|
+
|
|
375
|
+
new_session_id = finalize_autosave_session()
|
|
376
|
+
if not set_current_agent(agent_name):
|
|
377
|
+
emit_warning(
|
|
378
|
+
"Agent switch failed after autosave rotation. Your context was preserved.",
|
|
379
|
+
message_group=group_id,
|
|
380
|
+
)
|
|
381
|
+
return True
|
|
382
|
+
|
|
383
|
+
new_agent = get_current_agent()
|
|
384
|
+
new_agent.reload_code_generation_agent()
|
|
385
|
+
emit_success(
|
|
386
|
+
f"Switched to agent: {new_agent.display_name}",
|
|
387
|
+
message_group=group_id,
|
|
388
|
+
)
|
|
389
|
+
emit_info(f"{new_agent.description}", message_group=group_id)
|
|
390
|
+
emit_info(
|
|
391
|
+
Text.from_markup(
|
|
392
|
+
f"[dim]Auto-save session rotated to: {new_session_id}[/dim]"
|
|
393
|
+
),
|
|
394
|
+
message_group=group_id,
|
|
395
|
+
)
|
|
396
|
+
return True
|
|
397
|
+
else:
|
|
398
|
+
emit_warning("Usage: /agent [agent-name]")
|
|
399
|
+
return True
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
async def interactive_model_picker() -> str | None:
|
|
403
|
+
"""Show an interactive arrow-key selector to pick a model (async version).
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
The selected model name, or None if cancelled
|
|
407
|
+
"""
|
|
408
|
+
import sys
|
|
409
|
+
import time
|
|
410
|
+
|
|
411
|
+
from rich.console import Console
|
|
412
|
+
from rich.panel import Panel
|
|
413
|
+
from rich.text import Text
|
|
414
|
+
|
|
415
|
+
from code_puppy.command_line.model_picker_completion import (
|
|
416
|
+
get_active_model,
|
|
417
|
+
load_model_names,
|
|
418
|
+
)
|
|
419
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
420
|
+
from code_puppy.tools.common import arrow_select_async
|
|
421
|
+
|
|
422
|
+
# Load available models
|
|
423
|
+
model_names = load_model_names()
|
|
424
|
+
current_model = get_active_model()
|
|
425
|
+
|
|
426
|
+
# Build choices with current model indicator
|
|
427
|
+
choices = []
|
|
428
|
+
for model_name in model_names:
|
|
429
|
+
if model_name == current_model:
|
|
430
|
+
choices.append(f"✓ {model_name} (current)")
|
|
431
|
+
else:
|
|
432
|
+
choices.append(f" {model_name}")
|
|
433
|
+
|
|
434
|
+
# Create panel content
|
|
435
|
+
panel_content = Text()
|
|
436
|
+
panel_content.append("🤖 Select a model to use\n", style="bold cyan")
|
|
437
|
+
panel_content.append("Current model: ", style="dim")
|
|
438
|
+
panel_content.append(current_model, style="bold green")
|
|
439
|
+
|
|
440
|
+
# Display panel
|
|
441
|
+
panel = Panel(
|
|
442
|
+
panel_content,
|
|
443
|
+
title="[bold white]Model Selection[/bold white]",
|
|
444
|
+
border_style="cyan",
|
|
445
|
+
padding=(1, 2),
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Pause spinners BEFORE showing panel
|
|
449
|
+
set_awaiting_user_input(True)
|
|
450
|
+
time.sleep(0.3) # Let spinners fully stop
|
|
451
|
+
|
|
452
|
+
local_console = Console()
|
|
453
|
+
emit_info("")
|
|
454
|
+
local_console.print(panel)
|
|
455
|
+
emit_info("")
|
|
456
|
+
|
|
457
|
+
# Flush output before prompt_toolkit takes control
|
|
458
|
+
sys.stdout.flush()
|
|
459
|
+
sys.stderr.flush()
|
|
460
|
+
time.sleep(0.1)
|
|
461
|
+
|
|
462
|
+
selected_model = None
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
# Final flush
|
|
466
|
+
sys.stdout.flush()
|
|
467
|
+
|
|
468
|
+
# Show arrow-key selector (async version)
|
|
469
|
+
choice = await arrow_select_async(
|
|
470
|
+
"💭 Which model would you like to use?",
|
|
471
|
+
choices,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Extract model name from choice (remove prefix and suffix)
|
|
475
|
+
if choice:
|
|
476
|
+
# Remove the "✓ " or " " prefix and " (current)" suffix if present
|
|
477
|
+
selected_model = choice.strip().lstrip("✓").strip()
|
|
478
|
+
if selected_model.endswith(" (current)"):
|
|
479
|
+
selected_model = selected_model[:-10].strip()
|
|
480
|
+
|
|
481
|
+
except (KeyboardInterrupt, EOFError):
|
|
482
|
+
emit_error("Cancelled by user")
|
|
483
|
+
selected_model = None
|
|
484
|
+
|
|
485
|
+
finally:
|
|
486
|
+
set_awaiting_user_input(False)
|
|
487
|
+
|
|
488
|
+
return selected_model
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@register_command(
|
|
492
|
+
name="model",
|
|
493
|
+
description="Set active model",
|
|
494
|
+
usage="/model, /m <model>",
|
|
495
|
+
aliases=["m"],
|
|
496
|
+
category="core",
|
|
497
|
+
)
|
|
498
|
+
def handle_model_command(command: str) -> bool:
|
|
499
|
+
"""Set the active model."""
|
|
500
|
+
import asyncio
|
|
501
|
+
|
|
502
|
+
from code_puppy.command_line.model_picker_completion import (
|
|
503
|
+
get_active_model,
|
|
504
|
+
load_model_names,
|
|
505
|
+
set_active_model,
|
|
506
|
+
)
|
|
507
|
+
from code_puppy.messaging import emit_success, emit_warning
|
|
508
|
+
|
|
509
|
+
tokens = command.split()
|
|
510
|
+
|
|
511
|
+
# If just /model or /m with no args, show interactive picker
|
|
512
|
+
if len(tokens) == 1:
|
|
513
|
+
try:
|
|
514
|
+
# Run the async picker using asyncio utilities
|
|
515
|
+
# Since we're called from an async context but this function is sync,
|
|
516
|
+
# we need to carefully schedule and wait for the coroutine
|
|
517
|
+
import concurrent.futures
|
|
518
|
+
|
|
519
|
+
# Create a new event loop in a thread and run the picker there
|
|
520
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
521
|
+
future = executor.submit(
|
|
522
|
+
lambda: asyncio.run(interactive_model_picker())
|
|
523
|
+
)
|
|
524
|
+
selected_model = future.result(timeout=300) # 5 min timeout
|
|
525
|
+
|
|
526
|
+
if selected_model:
|
|
527
|
+
set_active_model(selected_model)
|
|
528
|
+
emit_success(f"Active model set and loaded: {selected_model}")
|
|
529
|
+
else:
|
|
530
|
+
emit_warning("Model selection cancelled")
|
|
531
|
+
return True
|
|
532
|
+
except Exception as e:
|
|
533
|
+
# Fallback to old behavior if picker fails
|
|
534
|
+
import traceback
|
|
535
|
+
|
|
536
|
+
emit_warning(f"Interactive picker failed: {e}")
|
|
537
|
+
emit_warning(f"Traceback: {traceback.format_exc()}")
|
|
538
|
+
model_names = load_model_names()
|
|
539
|
+
emit_warning("Usage: /model <model-name> or /m <model-name>")
|
|
540
|
+
emit_warning(f"Available models: {', '.join(model_names)}")
|
|
541
|
+
return True
|
|
542
|
+
|
|
543
|
+
# Handle both /model and /m for backward compatibility
|
|
544
|
+
model_command = command
|
|
545
|
+
if command.startswith("/model"):
|
|
546
|
+
# Convert /model to /m for internal processing
|
|
547
|
+
model_command = command.replace("/model", "/m", 1)
|
|
548
|
+
|
|
549
|
+
# If model matched, set it
|
|
550
|
+
new_input = update_model_in_input(model_command)
|
|
551
|
+
if new_input is not None:
|
|
552
|
+
model = get_active_model()
|
|
553
|
+
emit_success(f"Active model set and loaded: {model}")
|
|
554
|
+
return True
|
|
555
|
+
|
|
556
|
+
# If no model matched, show error
|
|
557
|
+
model_names = load_model_names()
|
|
558
|
+
emit_warning("Usage: /model <model-name> or /m <model-name>")
|
|
559
|
+
emit_warning(f"Available models: {', '.join(model_names)}")
|
|
560
|
+
return True
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@register_command(
|
|
564
|
+
name="add_model",
|
|
565
|
+
description="Browse and add models from models.dev catalog",
|
|
566
|
+
usage="/add_model",
|
|
567
|
+
category="core",
|
|
568
|
+
)
|
|
569
|
+
def handle_add_model_command(command: str) -> bool:
|
|
570
|
+
"""Launch interactive model browser TUI."""
|
|
571
|
+
from code_puppy.command_line.add_model_menu import interactive_model_picker
|
|
572
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
573
|
+
|
|
574
|
+
set_awaiting_user_input(True)
|
|
575
|
+
try:
|
|
576
|
+
# interactive_model_picker is now synchronous - no async complications!
|
|
577
|
+
result = interactive_model_picker()
|
|
578
|
+
|
|
579
|
+
if result:
|
|
580
|
+
emit_info("Successfully added model configuration")
|
|
581
|
+
return True
|
|
582
|
+
except KeyboardInterrupt:
|
|
583
|
+
# User cancelled - this is expected behavior
|
|
584
|
+
return True
|
|
585
|
+
except Exception as e:
|
|
586
|
+
emit_error(f"Failed to launch model browser: {e}")
|
|
587
|
+
return False
|
|
588
|
+
finally:
|
|
589
|
+
set_awaiting_user_input(False)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@register_command(
|
|
593
|
+
name="model_settings",
|
|
594
|
+
description="Configure per-model settings (temperature, seed, etc.)",
|
|
595
|
+
usage="/model_settings [--show [model_name]]",
|
|
596
|
+
aliases=["ms"],
|
|
597
|
+
category="config",
|
|
598
|
+
)
|
|
599
|
+
def handle_model_settings_command(command: str) -> bool:
|
|
600
|
+
"""Launch interactive model settings TUI.
|
|
601
|
+
|
|
602
|
+
Opens a TUI showing all available models. Select a model to configure
|
|
603
|
+
its settings (temperature, seed, etc.). ESC closes the TUI.
|
|
604
|
+
|
|
605
|
+
Use --show [model_name] to display current settings without the TUI.
|
|
606
|
+
"""
|
|
607
|
+
from code_puppy.command_line.model_settings_menu import (
|
|
608
|
+
interactive_model_settings,
|
|
609
|
+
show_model_settings_summary,
|
|
610
|
+
)
|
|
611
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
612
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
613
|
+
|
|
614
|
+
tokens = command.split()
|
|
615
|
+
|
|
616
|
+
# Check for --show flag to just display current settings
|
|
617
|
+
if "--show" in tokens:
|
|
618
|
+
model_name = None
|
|
619
|
+
for t in tokens[1:]:
|
|
620
|
+
if not t.startswith("--"):
|
|
621
|
+
model_name = t
|
|
622
|
+
break
|
|
623
|
+
show_model_settings_summary(model_name)
|
|
624
|
+
return True
|
|
625
|
+
|
|
626
|
+
set_awaiting_user_input(True)
|
|
627
|
+
try:
|
|
628
|
+
result = interactive_model_settings()
|
|
629
|
+
|
|
630
|
+
if result:
|
|
631
|
+
emit_success("Model settings updated successfully")
|
|
632
|
+
|
|
633
|
+
# Always reload the active agent so settings take effect
|
|
634
|
+
from code_puppy.agents import get_current_agent
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
current_agent = get_current_agent()
|
|
638
|
+
current_agent.reload_code_generation_agent()
|
|
639
|
+
emit_info("Active agent reloaded")
|
|
640
|
+
except Exception as reload_error:
|
|
641
|
+
emit_warning(f"Agent reload failed: {reload_error}")
|
|
642
|
+
|
|
643
|
+
return True
|
|
644
|
+
except KeyboardInterrupt:
|
|
645
|
+
return True
|
|
646
|
+
except Exception as e:
|
|
647
|
+
emit_error(f"Failed to launch model settings: {e}")
|
|
648
|
+
return False
|
|
649
|
+
finally:
|
|
650
|
+
set_awaiting_user_input(False)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@register_command(
|
|
654
|
+
name="mcp",
|
|
655
|
+
description="Manage MCP servers (list, start, stop, status, etc.)",
|
|
656
|
+
usage="/mcp",
|
|
657
|
+
category="core",
|
|
658
|
+
)
|
|
659
|
+
def handle_mcp_command(command: str) -> bool:
|
|
660
|
+
"""Handle MCP server management."""
|
|
661
|
+
from code_puppy.command_line.mcp import MCPCommandHandler
|
|
662
|
+
|
|
663
|
+
handler = MCPCommandHandler()
|
|
664
|
+
return handler.handle_mcp_command(command)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@register_command(
|
|
668
|
+
name="api",
|
|
669
|
+
description="Manage the Code Puppy API server",
|
|
670
|
+
usage="/api [start|stop|status]",
|
|
671
|
+
category="core",
|
|
672
|
+
detailed_help="Start, stop, or check status of the local FastAPI server for GUI integration.",
|
|
673
|
+
)
|
|
674
|
+
def handle_api_command(command: str) -> bool:
|
|
675
|
+
"""Handle the /api command."""
|
|
676
|
+
import os
|
|
677
|
+
import signal
|
|
678
|
+
import subprocess
|
|
679
|
+
import sys
|
|
680
|
+
from pathlib import Path
|
|
681
|
+
|
|
682
|
+
from code_puppy.config import STATE_DIR
|
|
683
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success
|
|
684
|
+
|
|
685
|
+
parts = command.split()
|
|
686
|
+
subcommand = parts[1] if len(parts) > 1 else "status"
|
|
687
|
+
|
|
688
|
+
pid_file = Path(STATE_DIR) / "api_server.pid"
|
|
689
|
+
|
|
690
|
+
if subcommand == "start":
|
|
691
|
+
# Check if already running
|
|
692
|
+
if pid_file.exists():
|
|
693
|
+
try:
|
|
694
|
+
pid = int(pid_file.read_text().strip())
|
|
695
|
+
os.kill(pid, 0) # Check if process exists
|
|
696
|
+
emit_info(f"API server already running (PID {pid})")
|
|
697
|
+
return True
|
|
698
|
+
except (OSError, ValueError):
|
|
699
|
+
pid_file.unlink(missing_ok=True) # Stale PID file
|
|
700
|
+
|
|
701
|
+
# Start the server in background
|
|
702
|
+
emit_info("Starting API server on http://127.0.0.1:8765 ...")
|
|
703
|
+
proc = subprocess.Popen(
|
|
704
|
+
[sys.executable, "-m", "code_puppy.api.main"],
|
|
705
|
+
stdout=subprocess.DEVNULL,
|
|
706
|
+
stderr=subprocess.DEVNULL,
|
|
707
|
+
start_new_session=True,
|
|
708
|
+
)
|
|
709
|
+
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
710
|
+
pid_file.write_text(str(proc.pid))
|
|
711
|
+
emit_success(f"API server started (PID {proc.pid})")
|
|
712
|
+
emit_info("Docs available at http://127.0.0.1:8765/docs")
|
|
713
|
+
return True
|
|
714
|
+
|
|
715
|
+
elif subcommand == "stop":
|
|
716
|
+
if not pid_file.exists():
|
|
717
|
+
emit_info("API server is not running")
|
|
718
|
+
return True
|
|
719
|
+
|
|
720
|
+
try:
|
|
721
|
+
pid = int(pid_file.read_text().strip())
|
|
722
|
+
os.kill(pid, signal.SIGTERM)
|
|
723
|
+
pid_file.unlink()
|
|
724
|
+
emit_success(f"API server stopped (PID {pid})")
|
|
725
|
+
except (OSError, ValueError) as e:
|
|
726
|
+
pid_file.unlink(missing_ok=True)
|
|
727
|
+
emit_error(f"Error stopping server: {e}")
|
|
728
|
+
return True
|
|
729
|
+
|
|
730
|
+
elif subcommand == "status":
|
|
731
|
+
if not pid_file.exists():
|
|
732
|
+
emit_info("API server is not running")
|
|
733
|
+
return True
|
|
734
|
+
|
|
735
|
+
try:
|
|
736
|
+
pid = int(pid_file.read_text().strip())
|
|
737
|
+
os.kill(pid, 0) # Check if process exists
|
|
738
|
+
emit_success(f"API server is running (PID {pid})")
|
|
739
|
+
emit_info("URL: http://127.0.0.1:8765")
|
|
740
|
+
emit_info("Docs: http://127.0.0.1:8765/docs")
|
|
741
|
+
except (OSError, ValueError):
|
|
742
|
+
pid_file.unlink(missing_ok=True)
|
|
743
|
+
emit_info("API server is not running (stale PID file removed)")
|
|
744
|
+
return True
|
|
745
|
+
|
|
746
|
+
else:
|
|
747
|
+
emit_error(f"Unknown subcommand: {subcommand}")
|
|
748
|
+
emit_info("Usage: /api [start|stop|status]")
|
|
749
|
+
return True
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@register_command(
|
|
753
|
+
name="generate-pr-description",
|
|
754
|
+
description="Generate comprehensive PR description",
|
|
755
|
+
usage="/generate-pr-description [@dir]",
|
|
756
|
+
category="core",
|
|
757
|
+
)
|
|
758
|
+
def handle_generate_pr_description_command(command: str) -> str:
|
|
759
|
+
"""Generate a PR description."""
|
|
760
|
+
# Parse directory argument (e.g., /generate-pr-description @some/dir)
|
|
761
|
+
tokens = command.split()
|
|
762
|
+
directory_context = ""
|
|
763
|
+
for t in tokens:
|
|
764
|
+
if t.startswith("@"):
|
|
765
|
+
directory_context = f" Please work in the directory: {t[1:]}"
|
|
766
|
+
break
|
|
767
|
+
|
|
768
|
+
# Hard-coded prompt from user requirements
|
|
769
|
+
pr_prompt = f"""Generate a comprehensive PR description for my current branch changes. Follow these steps:
|
|
770
|
+
|
|
771
|
+
1 Discover the changes: Use git CLI to find the base branch (usually main/master/develop) and get the list of changed files, commits, and diffs.
|
|
772
|
+
2 Analyze the code: Read and analyze all modified files to understand:
|
|
773
|
+
• What functionality was added/changed/removed
|
|
774
|
+
• The technical approach and implementation details
|
|
775
|
+
• Any architectural or design pattern changes
|
|
776
|
+
• Dependencies added/removed/updated
|
|
777
|
+
3 Generate a structured PR description with these sections:
|
|
778
|
+
• Title: Concise, descriptive title (50 chars max)
|
|
779
|
+
• Summary: Brief overview of what this PR accomplishes
|
|
780
|
+
• Changes Made: Detailed bullet points of specific changes
|
|
781
|
+
• Technical Details: Implementation approach, design decisions, patterns used
|
|
782
|
+
• Files Modified: List of key files with brief description of changes
|
|
783
|
+
• Testing: What was tested and how (if applicable)
|
|
784
|
+
• Breaking Changes: Any breaking changes (if applicable)
|
|
785
|
+
• Additional Notes: Any other relevant information
|
|
786
|
+
4 Create a markdown file: Generate a PR_DESCRIPTION.md file with proper GitHub markdown formatting that I can directly copy-paste into GitHub's PR
|
|
787
|
+
description field. Use proper markdown syntax with headers, bullet points, code blocks, and formatting.
|
|
788
|
+
5 Make it review-ready: Ensure the description helps reviewers understand the context, approach, and impact of the changes.
|
|
789
|
+
6. If you have Github MCP, or gh cli is installed and authenticated then find the PR for the branch we analyzed and update the PR description there and then delete the PR_DESCRIPTION.md file. (If you have a better name (title) for the PR, go ahead and update the title too.{directory_context}"""
|
|
790
|
+
|
|
791
|
+
# Return the prompt to be processed by the main chat system
|
|
792
|
+
return pr_prompt
|