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
|
@@ -1,667 +1,271 @@
|
|
|
1
|
-
|
|
1
|
+
# Import to trigger command registration
|
|
2
|
+
import code_puppy.command_line.config_commands # noqa: F401
|
|
3
|
+
import code_puppy.command_line.core_commands # noqa: F401
|
|
4
|
+
import code_puppy.command_line.session_commands # noqa: F401
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from code_puppy.command_line.utils import make_directory_table
|
|
6
|
-
from code_puppy.config import get_config_keys
|
|
7
|
-
from code_puppy.tools.tools_content import tools_content
|
|
6
|
+
# Global flag to track if plugins have been loaded
|
|
7
|
+
_PLUGINS_LOADED = False
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def get_commands_help():
|
|
11
|
-
"""Generate commands help using Rich Text
|
|
11
|
+
"""Generate aligned commands help using Rich Text for safe markup.
|
|
12
|
+
|
|
13
|
+
Now dynamically generates help from the command registry!
|
|
14
|
+
Only shows two sections: Built-in Commands and Custom Commands.
|
|
15
|
+
"""
|
|
12
16
|
from rich.text import Text
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
18
|
+
from code_puppy.command_line.command_registry import get_unique_commands
|
|
19
|
+
|
|
20
|
+
# Ensure plugins are loaded so custom help can register
|
|
21
|
+
_ensure_plugins_loaded()
|
|
22
|
+
|
|
23
|
+
lines: list[Text] = []
|
|
24
|
+
# No global header needed - user already knows they're viewing help
|
|
25
|
+
|
|
26
|
+
# Collect all built-in commands (registered + legacy)
|
|
27
|
+
builtin_cmds: list[tuple[str, str]] = []
|
|
28
|
+
|
|
29
|
+
# Get registered commands (all categories are built-in)
|
|
30
|
+
registered_commands = get_unique_commands()
|
|
31
|
+
for cmd_info in sorted(registered_commands, key=lambda c: c.name):
|
|
32
|
+
builtin_cmds.append((cmd_info.usage, cmd_info.description))
|
|
33
|
+
|
|
34
|
+
# Get custom commands from plugins
|
|
35
|
+
custom_entries: list[tuple[str, str]] = []
|
|
36
|
+
try:
|
|
37
|
+
from code_puppy import callbacks
|
|
38
|
+
|
|
39
|
+
custom_help_results = callbacks.on_custom_command_help()
|
|
40
|
+
for res in custom_help_results:
|
|
41
|
+
if not res:
|
|
42
|
+
continue
|
|
43
|
+
# Format 1: Tuple with (command_name, description)
|
|
44
|
+
if isinstance(res, tuple) and len(res) == 2:
|
|
45
|
+
cmd_name = str(res[0])
|
|
46
|
+
custom_entries.append((f"/{cmd_name}", str(res[1])))
|
|
47
|
+
# Format 2: List of tuples or strings
|
|
48
|
+
elif isinstance(res, list):
|
|
49
|
+
# Check if it's a list of tuples (preferred format)
|
|
50
|
+
if res and isinstance(res[0], tuple) and len(res[0]) == 2:
|
|
51
|
+
for item in res:
|
|
52
|
+
if isinstance(item, tuple) and len(item) == 2:
|
|
53
|
+
cmd_name = str(item[0])
|
|
54
|
+
custom_entries.append((f"/{cmd_name}", str(item[1])))
|
|
55
|
+
# Format 3: List of strings (legacy format)
|
|
56
|
+
# Extract command from first line like "/command_name - Description"
|
|
57
|
+
elif res and isinstance(res[0], str) and res[0].startswith("/"):
|
|
58
|
+
first_line = res[0]
|
|
59
|
+
if " - " in first_line:
|
|
60
|
+
parts = first_line.split(" - ", 1)
|
|
61
|
+
cmd_name = parts[0].lstrip("/").strip()
|
|
62
|
+
description = parts[1].strip()
|
|
63
|
+
custom_entries.append((f"/{cmd_name}", description))
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
# Calculate global column width (longest command across ALL sections + padding)
|
|
68
|
+
all_commands = builtin_cmds + custom_entries
|
|
69
|
+
if all_commands:
|
|
70
|
+
max_cmd_width = max(len(cmd) for cmd, _ in all_commands)
|
|
71
|
+
column_width = max_cmd_width + 4 # Add 4 spaces padding
|
|
72
|
+
else:
|
|
73
|
+
column_width = 30
|
|
74
|
+
|
|
75
|
+
# Maximum description width before truncation (to prevent line wrapping)
|
|
76
|
+
max_desc_width = 80
|
|
77
|
+
|
|
78
|
+
def truncate_desc(desc: str, max_width: int) -> str:
|
|
79
|
+
"""Truncate description if too long, add ellipsis."""
|
|
80
|
+
if len(desc) <= max_width:
|
|
81
|
+
return desc
|
|
82
|
+
return desc[: max_width - 3] + "..."
|
|
83
|
+
|
|
84
|
+
# Display Built-in Commands section (starts immediately, no blank line)
|
|
85
|
+
lines.append(Text("Built-in Commands", style="bold magenta"))
|
|
86
|
+
for cmd, desc in sorted(builtin_cmds, key=lambda x: x[0]):
|
|
87
|
+
truncated_desc = truncate_desc(desc, max_desc_width)
|
|
88
|
+
left = Text(cmd.ljust(column_width), style="cyan")
|
|
89
|
+
right = Text(truncated_desc)
|
|
90
|
+
line = Text()
|
|
91
|
+
line.append_text(left)
|
|
92
|
+
line.append_text(right)
|
|
93
|
+
lines.append(line)
|
|
94
|
+
|
|
95
|
+
# Display Custom Commands section (if any)
|
|
96
|
+
if custom_entries:
|
|
97
|
+
lines.append(Text(""))
|
|
98
|
+
lines.append(Text("Custom Commands", style="bold magenta"))
|
|
99
|
+
for cmd, desc in sorted(custom_entries, key=lambda x: x[0]):
|
|
100
|
+
truncated_desc = truncate_desc(desc, max_desc_width)
|
|
101
|
+
left = Text(cmd.ljust(column_width), style="cyan")
|
|
102
|
+
right = Text(truncated_desc)
|
|
103
|
+
line = Text()
|
|
104
|
+
line.append_text(left)
|
|
105
|
+
line.append_text(right)
|
|
106
|
+
lines.append(line)
|
|
107
|
+
|
|
92
108
|
final_text = Text()
|
|
93
|
-
for i, line in enumerate(
|
|
109
|
+
for i, line in enumerate(lines):
|
|
94
110
|
if i > 0:
|
|
95
111
|
final_text.append("\n")
|
|
96
112
|
final_text.append_text(line)
|
|
97
113
|
|
|
98
|
-
|
|
114
|
+
# Add trailing newline for spacing before next prompt
|
|
115
|
+
final_text.append("\n")
|
|
99
116
|
|
|
117
|
+
return final_text
|
|
100
118
|
|
|
101
|
-
def handle_command(command: str):
|
|
102
|
-
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
103
|
-
|
|
104
|
-
"""
|
|
105
|
-
Handle commands prefixed with '/'.
|
|
106
119
|
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
# ============================================================================
|
|
121
|
+
# IMPORT BUILT-IN COMMAND HANDLERS
|
|
122
|
+
# ============================================================================
|
|
123
|
+
# All built-in command handlers have been split into category-specific files.
|
|
124
|
+
# These imports trigger their registration via @register_command decorators.
|
|
109
125
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
command = command.strip()
|
|
126
|
+
# ============================================================================
|
|
127
|
+
# UTILITY FUNCTIONS
|
|
128
|
+
# ============================================================================
|
|
114
129
|
|
|
115
|
-
if command.strip().startswith("/motd"):
|
|
116
|
-
print_motd(force=True)
|
|
117
|
-
return True
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
truncation,
|
|
126
|
-
)
|
|
127
|
-
from code_puppy.messaging import (
|
|
128
|
-
emit_error,
|
|
129
|
-
emit_info,
|
|
130
|
-
emit_success,
|
|
131
|
-
emit_warning,
|
|
132
|
-
)
|
|
133
|
-
from code_puppy.state_management import get_message_history, set_message_history
|
|
131
|
+
def _ensure_plugins_loaded() -> None:
|
|
132
|
+
global _PLUGINS_LOADED
|
|
133
|
+
if _PLUGINS_LOADED:
|
|
134
|
+
return
|
|
135
|
+
try:
|
|
136
|
+
from code_puppy import plugins
|
|
134
137
|
|
|
138
|
+
plugins.load_plugin_callbacks()
|
|
139
|
+
_PLUGINS_LOADED = True
|
|
140
|
+
except Exception as e:
|
|
141
|
+
# If plugins fail to load, continue gracefully but note it
|
|
135
142
|
try:
|
|
136
|
-
|
|
137
|
-
if not history:
|
|
138
|
-
emit_warning("No history to compact yet. Ask me something first!")
|
|
139
|
-
return True
|
|
140
|
-
|
|
141
|
-
before_tokens = sum(estimate_tokens_for_message(m) for m in history)
|
|
142
|
-
compaction_strategy = get_compaction_strategy()
|
|
143
|
-
protected_tokens = get_protected_token_count()
|
|
144
|
-
emit_info(
|
|
145
|
-
f"🤔 Compacting {len(history)} messages using {compaction_strategy} strategy... (~{before_tokens} tokens)"
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
if compaction_strategy == "truncation":
|
|
149
|
-
compacted = truncation(history, protected_tokens)
|
|
150
|
-
summarized_messages = [] # No summarization in truncation mode
|
|
151
|
-
else:
|
|
152
|
-
# Default to summarization
|
|
153
|
-
compacted, summarized_messages = summarize_messages(
|
|
154
|
-
history, with_protection=True
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
if not compacted:
|
|
158
|
-
emit_error("Compaction failed. History unchanged.")
|
|
159
|
-
return True
|
|
160
|
-
|
|
161
|
-
set_message_history(compacted)
|
|
162
|
-
|
|
163
|
-
after_tokens = sum(estimate_tokens_for_message(m) for m in compacted)
|
|
164
|
-
reduction_pct = (
|
|
165
|
-
((before_tokens - after_tokens) / before_tokens * 100)
|
|
166
|
-
if before_tokens > 0
|
|
167
|
-
else 0
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
strategy_info = (
|
|
171
|
-
f"using {compaction_strategy} strategy"
|
|
172
|
-
if compaction_strategy == "truncation"
|
|
173
|
-
else "via summarization"
|
|
174
|
-
)
|
|
175
|
-
emit_success(
|
|
176
|
-
f"✨ Done! History: {len(history)} → {len(compacted)} messages {strategy_info}\n"
|
|
177
|
-
f"🏦 Tokens: {before_tokens:,} → {after_tokens:,} ({reduction_pct:.1f}% reduction)"
|
|
178
|
-
)
|
|
179
|
-
return True
|
|
180
|
-
except Exception as e:
|
|
181
|
-
emit_error(f"/compact error: {e}")
|
|
182
|
-
return True
|
|
143
|
+
from code_puppy.messaging import emit_warning
|
|
183
144
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
table = make_directory_table()
|
|
189
|
-
emit_info(table)
|
|
190
|
-
except Exception as e:
|
|
191
|
-
emit_error(f"Error listing directory: {e}")
|
|
192
|
-
return True
|
|
193
|
-
elif len(tokens) == 2:
|
|
194
|
-
dirname = tokens[1]
|
|
195
|
-
target = os.path.expanduser(dirname)
|
|
196
|
-
if not os.path.isabs(target):
|
|
197
|
-
target = os.path.join(os.getcwd(), target)
|
|
198
|
-
if os.path.isdir(target):
|
|
199
|
-
os.chdir(target)
|
|
200
|
-
emit_success(f"Changed directory to: {target}")
|
|
201
|
-
else:
|
|
202
|
-
emit_error(f"Not a directory: {dirname}")
|
|
203
|
-
return True
|
|
204
|
-
|
|
205
|
-
if command.strip().startswith("/show"):
|
|
206
|
-
from code_puppy.agents import get_current_agent_config
|
|
207
|
-
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
208
|
-
from code_puppy.config import (
|
|
209
|
-
get_compaction_strategy,
|
|
210
|
-
get_compaction_threshold,
|
|
211
|
-
get_owner_name,
|
|
212
|
-
get_protected_token_count,
|
|
213
|
-
get_puppy_name,
|
|
214
|
-
get_yolo_mode,
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
puppy_name = get_puppy_name()
|
|
218
|
-
owner_name = get_owner_name()
|
|
219
|
-
model = get_active_model()
|
|
220
|
-
yolo_mode = get_yolo_mode()
|
|
221
|
-
protected_tokens = get_protected_token_count()
|
|
222
|
-
compaction_threshold = get_compaction_threshold()
|
|
223
|
-
compaction_strategy = get_compaction_strategy()
|
|
224
|
-
|
|
225
|
-
# Get current agent info
|
|
226
|
-
current_agent = get_current_agent_config()
|
|
227
|
-
|
|
228
|
-
status_msg = f"""[bold magenta]🐶 Puppy Status[/bold magenta]
|
|
229
|
-
|
|
230
|
-
[bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan]
|
|
231
|
-
[bold]owner_name:[/bold] [cyan]{owner_name}[/cyan]
|
|
232
|
-
[bold]current_agent:[/bold] [magenta]{current_agent.display_name}[/magenta]
|
|
233
|
-
[bold]model:[/bold] [green]{model}[/green]
|
|
234
|
-
[bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
|
|
235
|
-
[bold]protected_tokens:[/bold] [cyan]{protected_tokens:,}[/cyan] recent tokens preserved
|
|
236
|
-
[bold]compaction_threshold:[/bold] [cyan]{compaction_threshold:.1%}[/cyan] context usage triggers compaction
|
|
237
|
-
[bold]compaction_strategy:[/bold] [cyan]{compaction_strategy}[/cyan] (summarization or truncation)
|
|
238
|
-
|
|
239
|
-
"""
|
|
240
|
-
emit_info(status_msg)
|
|
241
|
-
return True
|
|
242
|
-
|
|
243
|
-
if command.startswith("/set"):
|
|
244
|
-
# Syntax: /set KEY=VALUE or /set KEY VALUE
|
|
245
|
-
from code_puppy.config import set_config_value
|
|
246
|
-
|
|
247
|
-
tokens = command.split(None, 2)
|
|
248
|
-
argstr = command[len("/set") :].strip()
|
|
249
|
-
key = None
|
|
250
|
-
value = None
|
|
251
|
-
if "=" in argstr:
|
|
252
|
-
key, value = argstr.split("=", 1)
|
|
253
|
-
key = key.strip()
|
|
254
|
-
value = value.strip()
|
|
255
|
-
elif len(tokens) >= 3:
|
|
256
|
-
key = tokens[1]
|
|
257
|
-
value = tokens[2]
|
|
258
|
-
elif len(tokens) == 2:
|
|
259
|
-
key = tokens[1]
|
|
260
|
-
value = ""
|
|
261
|
-
else:
|
|
262
|
-
config_keys = get_config_keys()
|
|
263
|
-
if "compaction_strategy" not in config_keys:
|
|
264
|
-
config_keys.append("compaction_strategy")
|
|
265
|
-
emit_warning(
|
|
266
|
-
f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]"
|
|
267
|
-
)
|
|
268
|
-
return True
|
|
269
|
-
if key:
|
|
270
|
-
set_config_value(key, value)
|
|
271
|
-
emit_success(f'🌶 Set {key} = "{value}" in puppy.cfg!')
|
|
272
|
-
else:
|
|
273
|
-
emit_error("You must supply a key.")
|
|
274
|
-
return True
|
|
275
|
-
|
|
276
|
-
if command.startswith("/tools"):
|
|
277
|
-
# Display the tools_content.py file content with markdown formatting
|
|
278
|
-
from rich.markdown import Markdown
|
|
279
|
-
|
|
280
|
-
markdown_content = Markdown(tools_content)
|
|
281
|
-
emit_info(markdown_content)
|
|
282
|
-
return True
|
|
283
|
-
|
|
284
|
-
if command.startswith("/agent"):
|
|
285
|
-
# Handle agent switching
|
|
286
|
-
from code_puppy.agents import (
|
|
287
|
-
get_agent_descriptions,
|
|
288
|
-
get_available_agents,
|
|
289
|
-
get_current_agent_config,
|
|
290
|
-
set_current_agent,
|
|
291
|
-
)
|
|
292
|
-
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
293
|
-
|
|
294
|
-
tokens = command.split()
|
|
295
|
-
|
|
296
|
-
if len(tokens) == 1:
|
|
297
|
-
# Show current agent and available agents
|
|
298
|
-
current_agent = get_current_agent_config()
|
|
299
|
-
available_agents = get_available_agents()
|
|
300
|
-
descriptions = get_agent_descriptions()
|
|
145
|
+
emit_warning(f"Plugin load error: {e}")
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
_PLUGINS_LOADED = True
|
|
301
149
|
|
|
302
|
-
# Generate a group ID for all messages in this command
|
|
303
|
-
import uuid
|
|
304
150
|
|
|
305
|
-
|
|
151
|
+
# All command handlers moved to builtin_commands.py
|
|
152
|
+
# The import above triggers their registration
|
|
306
153
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
emit_info(
|
|
312
|
-
f"[dim]{current_agent.description}[/dim]\n", message_group=group_id
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
emit_info(
|
|
316
|
-
"[bold magenta]Available Agents:[/bold magenta]", message_group=group_id
|
|
317
|
-
)
|
|
318
|
-
for name, display_name in available_agents.items():
|
|
319
|
-
description = descriptions.get(name, "No description")
|
|
320
|
-
current_marker = (
|
|
321
|
-
" [green]← current[/green]" if name == current_agent.name else ""
|
|
322
|
-
)
|
|
323
|
-
emit_info(
|
|
324
|
-
f" [cyan]{name:<12}[/cyan] {display_name}{current_marker}",
|
|
325
|
-
message_group=group_id,
|
|
326
|
-
)
|
|
327
|
-
emit_info(f" [dim]{description}[/dim]", message_group=group_id)
|
|
154
|
+
# ============================================================================
|
|
155
|
+
# MAIN COMMAND DISPATCHER
|
|
156
|
+
# ============================================================================
|
|
328
157
|
|
|
329
|
-
|
|
330
|
-
"\n[yellow]Usage:[/yellow] /agent <agent-name>", message_group=group_id
|
|
331
|
-
)
|
|
332
|
-
return True
|
|
158
|
+
# _show_color_options has been moved to builtin_commands.py
|
|
333
159
|
|
|
334
|
-
elif len(tokens) == 2:
|
|
335
|
-
agent_name = tokens[1].lower()
|
|
336
160
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
group_id = str(uuid.uuid4())
|
|
341
|
-
|
|
342
|
-
if set_current_agent(agent_name):
|
|
343
|
-
# Reload the agent with new configuration
|
|
344
|
-
manager = get_runtime_agent_manager()
|
|
345
|
-
manager.reload_agent()
|
|
346
|
-
new_agent = get_current_agent_config()
|
|
347
|
-
emit_success(
|
|
348
|
-
f"Switched to agent: {new_agent.display_name}",
|
|
349
|
-
message_group=group_id,
|
|
350
|
-
)
|
|
351
|
-
emit_info(f"[dim]{new_agent.description}[/dim]", message_group=group_id)
|
|
352
|
-
return True
|
|
353
|
-
else:
|
|
354
|
-
# Generate a group ID for all messages in this command
|
|
355
|
-
import uuid
|
|
356
|
-
|
|
357
|
-
group_id = str(uuid.uuid4())
|
|
358
|
-
|
|
359
|
-
available_agents = get_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
|
-
else:
|
|
367
|
-
emit_warning("Usage: /agent [agent-name]")
|
|
368
|
-
return True
|
|
369
|
-
|
|
370
|
-
if command.startswith("/model") or command.startswith("/m "):
|
|
371
|
-
# Try setting model and show confirmation
|
|
372
|
-
# Handle both /model and /m for backward compatibility
|
|
373
|
-
model_command = command
|
|
374
|
-
if command.startswith("/model"):
|
|
375
|
-
# Convert /model to /m for internal processing
|
|
376
|
-
model_command = command.replace("/model", "/m", 1)
|
|
377
|
-
|
|
378
|
-
# If no model matched, show available models
|
|
379
|
-
from code_puppy.command_line.model_picker_completion import load_model_names
|
|
380
|
-
|
|
381
|
-
new_input = update_model_in_input(model_command)
|
|
382
|
-
if new_input is not None:
|
|
383
|
-
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
384
|
-
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
385
|
-
|
|
386
|
-
model = get_active_model()
|
|
387
|
-
# Make sure this is called for the test
|
|
388
|
-
manager = get_runtime_agent_manager()
|
|
389
|
-
manager.reload_agent()
|
|
390
|
-
emit_success(f"Active model set and loaded: {model}")
|
|
391
|
-
return True
|
|
392
|
-
model_names = load_model_names()
|
|
393
|
-
emit_warning("Usage: /model <model-name> or /m <model-name>")
|
|
394
|
-
emit_warning(f"Available models: {', '.join(model_names)}")
|
|
395
|
-
return True
|
|
396
|
-
|
|
397
|
-
if command.startswith("/mcp"):
|
|
398
|
-
from code_puppy.command_line.mcp import MCPCommandHandler
|
|
399
|
-
|
|
400
|
-
handler = MCPCommandHandler()
|
|
401
|
-
return handler.handle_mcp_command(command)
|
|
402
|
-
if command in ("/help", "/h"):
|
|
403
|
-
import uuid
|
|
404
|
-
|
|
405
|
-
group_id = str(uuid.uuid4())
|
|
406
|
-
help_text = get_commands_help()
|
|
407
|
-
emit_info(help_text, message_group_id=group_id)
|
|
408
|
-
return True
|
|
409
|
-
|
|
410
|
-
if command.startswith("/pin_model"):
|
|
411
|
-
# Handle agent model pinning
|
|
412
|
-
from code_puppy.agents.json_agent import discover_json_agents
|
|
413
|
-
from code_puppy.command_line.model_picker_completion import load_model_names
|
|
414
|
-
import json
|
|
415
|
-
|
|
416
|
-
tokens = command.split()
|
|
417
|
-
|
|
418
|
-
if len(tokens) != 3:
|
|
419
|
-
emit_warning("Usage: /pin_model <agent-name> <model-name>")
|
|
420
|
-
|
|
421
|
-
# Show available models and JSON agents
|
|
422
|
-
available_models = load_model_names()
|
|
423
|
-
json_agents = discover_json_agents()
|
|
424
|
-
|
|
425
|
-
emit_info("Available models:")
|
|
426
|
-
for model in available_models:
|
|
427
|
-
emit_info(f" [cyan]{model}[/cyan]")
|
|
428
|
-
|
|
429
|
-
if json_agents:
|
|
430
|
-
emit_info("\nAvailable JSON agents:")
|
|
431
|
-
for agent_name, agent_path in json_agents.items():
|
|
432
|
-
emit_info(f" [cyan]{agent_name}[/cyan] ({agent_path})")
|
|
433
|
-
return True
|
|
434
|
-
|
|
435
|
-
agent_name = tokens[1].lower()
|
|
436
|
-
model_name = tokens[2]
|
|
437
|
-
|
|
438
|
-
# Check if model exists
|
|
439
|
-
available_models = load_model_names()
|
|
440
|
-
if model_name not in available_models:
|
|
441
|
-
emit_error(f"Model '{model_name}' not found")
|
|
442
|
-
emit_warning(f"Available models: {', '.join(available_models)}")
|
|
443
|
-
return True
|
|
444
|
-
|
|
445
|
-
# Check that we're modifying a JSON agent (not a built-in Python agent)
|
|
446
|
-
json_agents = discover_json_agents()
|
|
447
|
-
if agent_name not in json_agents:
|
|
448
|
-
emit_error(f"JSON agent '{agent_name}' not found")
|
|
449
|
-
|
|
450
|
-
# Show available JSON agents
|
|
451
|
-
if json_agents:
|
|
452
|
-
emit_info("Available JSON agents:")
|
|
453
|
-
for name, path in json_agents.items():
|
|
454
|
-
emit_info(f" [cyan]{name}[/cyan] ({path})")
|
|
455
|
-
return True
|
|
456
|
-
|
|
457
|
-
agent_file_path = json_agents[agent_name]
|
|
458
|
-
|
|
459
|
-
# Load, modify, and save the agent configuration
|
|
460
|
-
try:
|
|
461
|
-
with open(agent_file_path, "r", encoding="utf-8") as f:
|
|
462
|
-
agent_config = json.load(f)
|
|
463
|
-
|
|
464
|
-
# Set the model
|
|
465
|
-
agent_config["model"] = model_name
|
|
466
|
-
|
|
467
|
-
# Save the updated configuration
|
|
468
|
-
with open(agent_file_path, "w", encoding="utf-8") as f:
|
|
469
|
-
json.dump(agent_config, f, indent=2, ensure_ascii=False)
|
|
470
|
-
|
|
471
|
-
emit_success(f"Model '{model_name}' pinned to agent '{agent_name}'")
|
|
472
|
-
|
|
473
|
-
# If this is the current agent, reload it to use the new model
|
|
474
|
-
from code_puppy.agents import get_current_agent_config
|
|
475
|
-
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
161
|
+
def handle_command(command: str):
|
|
162
|
+
"""
|
|
163
|
+
Handle commands prefixed with '/'.
|
|
476
164
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
manager = get_runtime_agent_manager()
|
|
480
|
-
manager.reload_agent()
|
|
481
|
-
emit_info(f"Active agent reloaded with pinned model '{model_name}'")
|
|
165
|
+
Args:
|
|
166
|
+
command: The command string to handle
|
|
482
167
|
|
|
483
|
-
|
|
168
|
+
Returns:
|
|
169
|
+
True if the command was handled, False if not, or a string to be processed as user input
|
|
170
|
+
"""
|
|
171
|
+
from rich.text import Text
|
|
484
172
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
return True
|
|
488
|
-
|
|
489
|
-
if command.startswith("/generate-pr-description"):
|
|
490
|
-
# Parse directory argument (e.g., /generate-pr-description @some/dir)
|
|
491
|
-
tokens = command.split()
|
|
492
|
-
directory_context = ""
|
|
493
|
-
for t in tokens:
|
|
494
|
-
if t.startswith("@"):
|
|
495
|
-
directory_context = f" Please work in the directory: {t[1:]}"
|
|
496
|
-
break
|
|
497
|
-
|
|
498
|
-
# Hard-coded prompt from user requirements
|
|
499
|
-
pr_prompt = f"""Generate a comprehensive PR description for my current branch changes. Follow these steps:
|
|
500
|
-
|
|
501
|
-
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.
|
|
502
|
-
2 Analyze the code: Read and analyze all modified files to understand:
|
|
503
|
-
• What functionality was added/changed/removed
|
|
504
|
-
• The technical approach and implementation details
|
|
505
|
-
• Any architectural or design pattern changes
|
|
506
|
-
• Dependencies added/removed/updated
|
|
507
|
-
3 Generate a structured PR description with these sections:
|
|
508
|
-
• Title: Concise, descriptive title (50 chars max)
|
|
509
|
-
• Summary: Brief overview of what this PR accomplishes
|
|
510
|
-
• Changes Made: Detailed bullet points of specific changes
|
|
511
|
-
• Technical Details: Implementation approach, design decisions, patterns used
|
|
512
|
-
• Files Modified: List of key files with brief description of changes
|
|
513
|
-
• Testing: What was tested and how (if applicable)
|
|
514
|
-
• Breaking Changes: Any breaking changes (if applicable)
|
|
515
|
-
• Additional Notes: Any other relevant information
|
|
516
|
-
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
|
|
517
|
-
description field. Use proper markdown syntax with headers, bullet points, code blocks, and formatting.
|
|
518
|
-
5 Make it review-ready: Ensure the description helps reviewers understand the context, approach, and impact of the changes.
|
|
519
|
-
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}"""
|
|
520
|
-
|
|
521
|
-
# Return the prompt to be processed by the main chat system
|
|
522
|
-
return pr_prompt
|
|
523
|
-
|
|
524
|
-
if command.startswith("/dump_context"):
|
|
525
|
-
import json
|
|
526
|
-
import pickle
|
|
527
|
-
from datetime import datetime
|
|
528
|
-
from pathlib import Path
|
|
529
|
-
|
|
530
|
-
from code_puppy.config import CONFIG_DIR
|
|
531
|
-
from code_puppy.message_history_processor import estimate_tokens_for_message
|
|
532
|
-
from code_puppy.state_management import get_message_history
|
|
533
|
-
|
|
534
|
-
tokens = command.split()
|
|
535
|
-
if len(tokens) != 2:
|
|
536
|
-
emit_warning("Usage: /dump_context <session_name>")
|
|
537
|
-
return True
|
|
538
|
-
|
|
539
|
-
session_name = tokens[1]
|
|
540
|
-
history = get_message_history()
|
|
541
|
-
|
|
542
|
-
if not history:
|
|
543
|
-
emit_warning("No message history to dump!")
|
|
544
|
-
return True
|
|
545
|
-
|
|
546
|
-
# Create contexts directory inside CONFIG_DIR if it doesn't exist
|
|
547
|
-
contexts_dir = Path(CONFIG_DIR) / "contexts"
|
|
548
|
-
contexts_dir.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
from code_puppy.command_line.command_registry import get_command
|
|
174
|
+
from code_puppy.messaging import emit_info, emit_warning
|
|
549
175
|
|
|
550
|
-
|
|
551
|
-
# Save as pickle for exact preservation
|
|
552
|
-
pickle_file = contexts_dir / f"{session_name}.pkl"
|
|
553
|
-
with open(pickle_file, "wb") as f:
|
|
554
|
-
pickle.dump(history, f)
|
|
555
|
-
|
|
556
|
-
# Also save metadata as JSON for readability
|
|
557
|
-
meta_file = contexts_dir / f"{session_name}_meta.json"
|
|
558
|
-
metadata = {
|
|
559
|
-
"session_name": session_name,
|
|
560
|
-
"timestamp": datetime.now().isoformat(),
|
|
561
|
-
"message_count": len(history),
|
|
562
|
-
"total_tokens": sum(estimate_tokens_for_message(m) for m in history),
|
|
563
|
-
"file_path": str(pickle_file),
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
with open(meta_file, "w") as f:
|
|
567
|
-
json.dump(metadata, f, indent=2)
|
|
568
|
-
|
|
569
|
-
emit_success(
|
|
570
|
-
f"✅ Context saved: {len(history)} messages ({metadata['total_tokens']} tokens)\n"
|
|
571
|
-
f"📁 Files: {pickle_file}, {meta_file}"
|
|
572
|
-
)
|
|
573
|
-
return True
|
|
176
|
+
_ensure_plugins_loaded()
|
|
574
177
|
|
|
575
|
-
|
|
576
|
-
emit_error(f"Failed to dump context: {e}")
|
|
577
|
-
return True
|
|
578
|
-
|
|
579
|
-
if command.startswith("/load_context"):
|
|
580
|
-
import pickle
|
|
581
|
-
from pathlib import Path
|
|
582
|
-
|
|
583
|
-
from code_puppy.config import CONFIG_DIR
|
|
584
|
-
from code_puppy.message_history_processor import estimate_tokens_for_message
|
|
585
|
-
from code_puppy.state_management import set_message_history
|
|
586
|
-
|
|
587
|
-
tokens = command.split()
|
|
588
|
-
if len(tokens) != 2:
|
|
589
|
-
emit_warning("Usage: /load_context <session_name>")
|
|
590
|
-
return True
|
|
591
|
-
|
|
592
|
-
session_name = tokens[1]
|
|
593
|
-
contexts_dir = Path(CONFIG_DIR) / "contexts"
|
|
594
|
-
pickle_file = contexts_dir / f"{session_name}.pkl"
|
|
595
|
-
|
|
596
|
-
if not pickle_file.exists():
|
|
597
|
-
emit_error(f"Context file not found: {pickle_file}")
|
|
598
|
-
# List available contexts
|
|
599
|
-
available = list(contexts_dir.glob("*.pkl"))
|
|
600
|
-
if available:
|
|
601
|
-
names = [f.stem for f in available]
|
|
602
|
-
emit_info(f"Available contexts: {', '.join(names)}")
|
|
603
|
-
return True
|
|
178
|
+
command = command.strip()
|
|
604
179
|
|
|
180
|
+
# Check if this is a registered command
|
|
181
|
+
if command.startswith("/"):
|
|
182
|
+
# Extract command name (first word after /)
|
|
183
|
+
cmd_name = command[1:].split()[0] if len(command) > 1 else ""
|
|
184
|
+
|
|
185
|
+
# Try to find in registry
|
|
186
|
+
cmd_info = get_command(cmd_name)
|
|
187
|
+
if cmd_info:
|
|
188
|
+
# Execute the registered handler
|
|
189
|
+
return cmd_info.handler(command)
|
|
190
|
+
|
|
191
|
+
# ========================================================================
|
|
192
|
+
# LEGACY COMMAND FALLBACK
|
|
193
|
+
# ========================================================================
|
|
194
|
+
# This section is kept as a fallback mechanism for commands added in other
|
|
195
|
+
# branches that haven't been migrated to the registry system yet.
|
|
196
|
+
#
|
|
197
|
+
# All current commands are registered above using @register_command, so
|
|
198
|
+
# they won't fall through to this section.
|
|
199
|
+
#
|
|
200
|
+
# If you're rebasing and your branch adds a new command using the old
|
|
201
|
+
# if/elif style, it will still work! Just add your if block below.
|
|
202
|
+
#
|
|
203
|
+
# EXAMPLE: How to add a legacy command:
|
|
204
|
+
#
|
|
205
|
+
# if command.startswith("/mycommand"):
|
|
206
|
+
# from code_puppy.messaging import emit_info
|
|
207
|
+
# emit_info("My command executed!")
|
|
208
|
+
# return True
|
|
209
|
+
#
|
|
210
|
+
# NOTE: For new commands, please use @register_command instead (see above).
|
|
211
|
+
# ========================================================================
|
|
212
|
+
|
|
213
|
+
# Legacy commands from other branches/rebases go here:
|
|
214
|
+
# (All current commands are in the registry above)
|
|
215
|
+
|
|
216
|
+
# Example placeholder (remove this and add your command if needed):
|
|
217
|
+
# if command.startswith("/my_new_command"):
|
|
218
|
+
# from code_puppy.messaging import emit_info
|
|
219
|
+
# emit_info("Command executed!")
|
|
220
|
+
# return True
|
|
221
|
+
|
|
222
|
+
# End of legacy fallback section
|
|
223
|
+
# ========================================================================
|
|
224
|
+
|
|
225
|
+
# All legacy command implementations have been moved to @register_command handlers above.
|
|
226
|
+
# If you're adding a new command via rebase, add your if block here.
|
|
227
|
+
|
|
228
|
+
# Try plugin-provided custom commands before unknown warning
|
|
229
|
+
if command.startswith("/"):
|
|
230
|
+
# Extract command name without leading slash and arguments intact
|
|
231
|
+
name = command[1:].split()[0] if len(command) > 1 else ""
|
|
605
232
|
try:
|
|
606
|
-
|
|
607
|
-
history = pickle.load(f)
|
|
608
|
-
|
|
609
|
-
set_message_history(history)
|
|
610
|
-
total_tokens = sum(estimate_tokens_for_message(m) for m in history)
|
|
611
|
-
|
|
612
|
-
emit_success(
|
|
613
|
-
f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
|
614
|
-
f"📁 From: {pickle_file}"
|
|
615
|
-
)
|
|
616
|
-
return True
|
|
233
|
+
from code_puppy import callbacks
|
|
617
234
|
|
|
235
|
+
# Import the special result class for markdown commands
|
|
236
|
+
try:
|
|
237
|
+
from code_puppy.plugins.customizable_commands.register_callbacks import (
|
|
238
|
+
MarkdownCommandResult,
|
|
239
|
+
)
|
|
240
|
+
except ImportError:
|
|
241
|
+
MarkdownCommandResult = None
|
|
242
|
+
|
|
243
|
+
results = callbacks.on_custom_command(command=command, name=name)
|
|
244
|
+
# Iterate through callback results; treat str as handled (no model run)
|
|
245
|
+
for res in results:
|
|
246
|
+
if res is True:
|
|
247
|
+
return True
|
|
248
|
+
if MarkdownCommandResult and isinstance(res, MarkdownCommandResult):
|
|
249
|
+
# Special case: markdown command that should be processed as input
|
|
250
|
+
# Replace the command with the markdown content and let it be processed
|
|
251
|
+
# This is handled by the caller, so return the content as string
|
|
252
|
+
return res.content
|
|
253
|
+
if isinstance(res, str):
|
|
254
|
+
# Display returned text to the user and treat as handled
|
|
255
|
+
try:
|
|
256
|
+
emit_info(res)
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
259
|
+
return True
|
|
618
260
|
except Exception as e:
|
|
619
|
-
emit_error
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if command.startswith("/truncate"):
|
|
623
|
-
tokens = command.split()
|
|
624
|
-
if len(tokens) != 2:
|
|
625
|
-
emit_error("Usage: /truncate <N> (where N is the number of messages to keep)")
|
|
626
|
-
return True
|
|
627
|
-
|
|
628
|
-
try:
|
|
629
|
-
n = int(tokens[1])
|
|
630
|
-
if n < 1:
|
|
631
|
-
emit_error("N must be a positive integer")
|
|
632
|
-
return True
|
|
633
|
-
except ValueError:
|
|
634
|
-
emit_error("N must be a valid integer")
|
|
635
|
-
return True
|
|
636
|
-
|
|
637
|
-
from code_puppy.state_management import get_message_history, set_message_history
|
|
638
|
-
|
|
639
|
-
history = get_message_history()
|
|
640
|
-
if not history:
|
|
641
|
-
emit_warning("No history to truncate yet. Ask me something first!")
|
|
642
|
-
return True
|
|
643
|
-
|
|
644
|
-
if len(history) <= n:
|
|
645
|
-
emit_info(f"History already has {len(history)} messages, which is <= {n}. Nothing to truncate.")
|
|
646
|
-
return True
|
|
647
|
-
|
|
648
|
-
# Always keep the first message (system message) and then keep the N-1 most recent messages
|
|
649
|
-
truncated_history = [history[0]] + history[-(n-1):] if n > 1 else [history[0]]
|
|
650
|
-
|
|
651
|
-
set_message_history(truncated_history)
|
|
652
|
-
emit_success(f"Truncated message history from {len(history)} to {len(truncated_history)} messages (keeping system message and {n-1} most recent)")
|
|
653
|
-
return True
|
|
261
|
+
# Log via emit_error but do not block default handling
|
|
262
|
+
emit_warning(f"Custom command hook error: {e}")
|
|
654
263
|
|
|
655
|
-
if command in ("/exit", "/quit"):
|
|
656
|
-
emit_success("Goodbye!")
|
|
657
|
-
# Signal to the main app that we want to exit
|
|
658
|
-
# The actual exit handling is done in main.py
|
|
659
|
-
return True
|
|
660
|
-
if command.startswith("/"):
|
|
661
|
-
name = command[1:].split()[0] if len(command) > 1 else ""
|
|
662
264
|
if name:
|
|
663
265
|
emit_warning(
|
|
664
|
-
|
|
266
|
+
Text.from_markup(
|
|
267
|
+
f"Unknown command: {command}\n[dim]Type /help for options.[/dim]"
|
|
268
|
+
)
|
|
665
269
|
)
|
|
666
270
|
else:
|
|
667
271
|
# Show current model ONLY here
|
|
@@ -669,7 +273,9 @@ def handle_command(command: str):
|
|
|
669
273
|
|
|
670
274
|
current_model = get_active_model()
|
|
671
275
|
emit_info(
|
|
672
|
-
|
|
276
|
+
Text.from_markup(
|
|
277
|
+
f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
|
|
278
|
+
)
|
|
673
279
|
)
|
|
674
280
|
return True
|
|
675
281
|
|