code-puppy 0.0.214__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 +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- 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 +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- 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 +142 -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 +10 -5
- 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 +176 -738
- 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 +0 -3
- 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 +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- 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 +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- 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 +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- 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 +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- 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 +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- 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 +2 -2
- 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 +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- 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 +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- 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.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- 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 -185
- 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 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- 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 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,118 +1,131 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from code_puppy.config import (
|
|
9
|
-
CONTEXTS_DIR,
|
|
10
|
-
finalize_autosave_session,
|
|
11
|
-
get_config_keys,
|
|
12
|
-
)
|
|
13
|
-
from code_puppy.session_storage import list_sessions, load_session, save_session
|
|
14
|
-
from code_puppy.tools.tools_content import tools_content
|
|
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
|
|
5
|
+
|
|
6
|
+
# Global flag to track if plugins have been loaded
|
|
7
|
+
_PLUGINS_LOADED = False
|
|
15
8
|
|
|
16
9
|
|
|
17
10
|
def get_commands_help():
|
|
18
|
-
"""Generate aligned commands help using Rich Text for safe markup.
|
|
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
|
+
"""
|
|
19
16
|
from rich.text import Text
|
|
20
17
|
|
|
18
|
+
from code_puppy.command_line.command_registry import get_unique_commands
|
|
19
|
+
|
|
21
20
|
# Ensure plugins are loaded so custom help can register
|
|
22
21
|
_ensure_plugins_loaded()
|
|
23
22
|
|
|
24
|
-
# Collect core commands with their syntax parts and descriptions
|
|
25
|
-
# (cmd_syntax, description)
|
|
26
|
-
core_cmds = [
|
|
27
|
-
("/help, /h", "Show this help message"),
|
|
28
|
-
("/cd <dir>", "Change directory or show directories"),
|
|
29
|
-
(
|
|
30
|
-
"/agent <name>",
|
|
31
|
-
"Switch to a different agent or show available agents",
|
|
32
|
-
),
|
|
33
|
-
("/exit, /quit", "Exit interactive mode"),
|
|
34
|
-
("/generate-pr-description [@dir]", "Generate comprehensive PR description"),
|
|
35
|
-
("/model, /m <model>", "Set active model"),
|
|
36
|
-
(
|
|
37
|
-
"/reasoning <low|medium|high>",
|
|
38
|
-
"Set OpenAI reasoning effort for GPT-5 models",
|
|
39
|
-
),
|
|
40
|
-
("/pin_model <agent> <model>", "Pin a specific model to an agent"),
|
|
41
|
-
("/mcp", "Manage MCP servers (list, start, stop, status, etc.)"),
|
|
42
|
-
("/motd", "Show the latest message of the day (MOTD)"),
|
|
43
|
-
("/show", "Show puppy config key-values"),
|
|
44
|
-
(
|
|
45
|
-
"/compact",
|
|
46
|
-
"Summarize and compact current chat history (uses compaction_strategy config)",
|
|
47
|
-
),
|
|
48
|
-
("/dump_context <name>", "Save current message history to file"),
|
|
49
|
-
("/load_context <name>", "Load message history from file"),
|
|
50
|
-
(
|
|
51
|
-
"/set",
|
|
52
|
-
"Set puppy config (e.g., /set yolo_mode true, /set auto_save_session true)",
|
|
53
|
-
),
|
|
54
|
-
("/tools", "Show available tools and capabilities"),
|
|
55
|
-
(
|
|
56
|
-
"/truncate <N>",
|
|
57
|
-
"Truncate history to N most recent messages (keeping system message)",
|
|
58
|
-
),
|
|
59
|
-
("/<unknown>", "Show unknown command warning"),
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
# Determine padding width for the left column
|
|
63
|
-
left_width = max(len(cmd) for cmd, _ in core_cmds) + 2 # add spacing
|
|
64
|
-
|
|
65
23
|
lines: list[Text] = []
|
|
66
|
-
|
|
24
|
+
# No global header needed - user already knows they're viewing help
|
|
67
25
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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))
|
|
75
33
|
|
|
76
|
-
#
|
|
34
|
+
# Get custom commands from plugins
|
|
35
|
+
custom_entries: list[tuple[str, str]] = []
|
|
77
36
|
try:
|
|
78
37
|
from code_puppy import callbacks
|
|
79
38
|
|
|
80
39
|
custom_help_results = callbacks.on_custom_command_help()
|
|
81
|
-
custom_entries: list[tuple[str, str]] = []
|
|
82
40
|
for res in custom_help_results:
|
|
83
41
|
if not res:
|
|
84
42
|
continue
|
|
43
|
+
# Format 1: Tuple with (command_name, description)
|
|
85
44
|
if isinstance(res, tuple) and len(res) == 2:
|
|
86
|
-
|
|
45
|
+
cmd_name = str(res[0])
|
|
46
|
+
custom_entries.append((f"/{cmd_name}", str(res[1])))
|
|
47
|
+
# Format 2: List of tuples or strings
|
|
87
48
|
elif isinstance(res, list):
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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))
|
|
103
64
|
except Exception:
|
|
104
65
|
pass
|
|
105
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
|
+
|
|
106
108
|
final_text = Text()
|
|
107
109
|
for i, line in enumerate(lines):
|
|
108
110
|
if i > 0:
|
|
109
111
|
final_text.append("\n")
|
|
110
112
|
final_text.append_text(line)
|
|
111
113
|
|
|
114
|
+
# Add trailing newline for spacing before next prompt
|
|
115
|
+
final_text.append("\n")
|
|
116
|
+
|
|
112
117
|
return final_text
|
|
113
118
|
|
|
114
119
|
|
|
115
|
-
|
|
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.
|
|
125
|
+
|
|
126
|
+
# ============================================================================
|
|
127
|
+
# UTILITY FUNCTIONS
|
|
128
|
+
# ============================================================================
|
|
116
129
|
|
|
117
130
|
|
|
118
131
|
def _ensure_plugins_loaded() -> None:
|
|
@@ -135,11 +148,17 @@ def _ensure_plugins_loaded() -> None:
|
|
|
135
148
|
_PLUGINS_LOADED = True
|
|
136
149
|
|
|
137
150
|
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
# All command handlers moved to builtin_commands.py
|
|
152
|
+
# The import above triggers their registration
|
|
140
153
|
|
|
141
|
-
|
|
154
|
+
# ============================================================================
|
|
155
|
+
# MAIN COMMAND DISPATCHER
|
|
156
|
+
# ============================================================================
|
|
157
|
+
|
|
158
|
+
# _show_color_options has been moved to builtin_commands.py
|
|
142
159
|
|
|
160
|
+
|
|
161
|
+
def handle_command(command: str):
|
|
143
162
|
"""
|
|
144
163
|
Handle commands prefixed with '/'.
|
|
145
164
|
|
|
@@ -149,660 +168,62 @@ def handle_command(command: str):
|
|
|
149
168
|
Returns:
|
|
150
169
|
True if the command was handled, False if not, or a string to be processed as user input
|
|
151
170
|
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if command.strip().startswith("/motd"):
|
|
155
|
-
print_motd(force=True)
|
|
156
|
-
return True
|
|
157
|
-
|
|
158
|
-
if command.strip().startswith("/compact"):
|
|
159
|
-
# Functions have been moved to BaseAgent class
|
|
160
|
-
from code_puppy.agents.agent_manager import get_current_agent
|
|
161
|
-
from code_puppy.config import get_compaction_strategy, get_protected_token_count
|
|
162
|
-
from code_puppy.messaging import (
|
|
163
|
-
emit_error,
|
|
164
|
-
emit_info,
|
|
165
|
-
emit_success,
|
|
166
|
-
emit_warning,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
try:
|
|
170
|
-
agent = get_current_agent()
|
|
171
|
-
history = agent.get_message_history()
|
|
172
|
-
if not history:
|
|
173
|
-
emit_warning("No history to compact yet. Ask me something first!")
|
|
174
|
-
return True
|
|
175
|
-
|
|
176
|
-
current_agent = get_current_agent()
|
|
177
|
-
before_tokens = sum(
|
|
178
|
-
current_agent.estimate_tokens_for_message(m) for m in history
|
|
179
|
-
)
|
|
180
|
-
compaction_strategy = get_compaction_strategy()
|
|
181
|
-
protected_tokens = get_protected_token_count()
|
|
182
|
-
emit_info(
|
|
183
|
-
f"🤔 Compacting {len(history)} messages using {compaction_strategy} strategy... (~{before_tokens} tokens)"
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
current_agent = get_current_agent()
|
|
187
|
-
if compaction_strategy == "truncation":
|
|
188
|
-
compacted = current_agent.truncation(history, protected_tokens)
|
|
189
|
-
summarized_messages = [] # No summarization in truncation mode
|
|
190
|
-
else:
|
|
191
|
-
# Default to summarization
|
|
192
|
-
compacted, summarized_messages = current_agent.summarize_messages(
|
|
193
|
-
history, with_protection=True
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
if not compacted:
|
|
197
|
-
emit_error("Compaction failed. History unchanged.")
|
|
198
|
-
return True
|
|
199
|
-
|
|
200
|
-
agent.set_message_history(compacted)
|
|
201
|
-
|
|
202
|
-
current_agent = get_current_agent()
|
|
203
|
-
after_tokens = sum(
|
|
204
|
-
current_agent.estimate_tokens_for_message(m) for m in compacted
|
|
205
|
-
)
|
|
206
|
-
reduction_pct = (
|
|
207
|
-
((before_tokens - after_tokens) / before_tokens * 100)
|
|
208
|
-
if before_tokens > 0
|
|
209
|
-
else 0
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
strategy_info = (
|
|
213
|
-
f"using {compaction_strategy} strategy"
|
|
214
|
-
if compaction_strategy == "truncation"
|
|
215
|
-
else "via summarization"
|
|
216
|
-
)
|
|
217
|
-
emit_success(
|
|
218
|
-
f"✨ Done! History: {len(history)} → {len(compacted)} messages {strategy_info}\n"
|
|
219
|
-
f"🏦 Tokens: {before_tokens:,} → {after_tokens:,} ({reduction_pct:.1f}% reduction)"
|
|
220
|
-
)
|
|
221
|
-
return True
|
|
222
|
-
except Exception as e:
|
|
223
|
-
emit_error(f"/compact error: {e}")
|
|
224
|
-
return True
|
|
225
|
-
|
|
226
|
-
if command.startswith("/cd"):
|
|
227
|
-
tokens = command.split()
|
|
228
|
-
if len(tokens) == 1:
|
|
229
|
-
try:
|
|
230
|
-
table = make_directory_table()
|
|
231
|
-
emit_info(table)
|
|
232
|
-
except Exception as e:
|
|
233
|
-
emit_error(f"Error listing directory: {e}")
|
|
234
|
-
return True
|
|
235
|
-
elif len(tokens) == 2:
|
|
236
|
-
dirname = tokens[1]
|
|
237
|
-
target = os.path.expanduser(dirname)
|
|
238
|
-
if not os.path.isabs(target):
|
|
239
|
-
target = os.path.join(os.getcwd(), target)
|
|
240
|
-
if os.path.isdir(target):
|
|
241
|
-
os.chdir(target)
|
|
242
|
-
emit_success(f"Changed directory to: {target}")
|
|
243
|
-
else:
|
|
244
|
-
emit_error(f"Not a directory: {dirname}")
|
|
245
|
-
return True
|
|
246
|
-
|
|
247
|
-
if command.strip().startswith("/show"):
|
|
248
|
-
from code_puppy.agents import get_current_agent
|
|
249
|
-
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
250
|
-
from code_puppy.config import (
|
|
251
|
-
get_compaction_strategy,
|
|
252
|
-
get_compaction_threshold,
|
|
253
|
-
get_openai_reasoning_effort,
|
|
254
|
-
get_owner_name,
|
|
255
|
-
get_protected_token_count,
|
|
256
|
-
get_puppy_name,
|
|
257
|
-
get_use_dbos,
|
|
258
|
-
get_yolo_mode,
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
puppy_name = get_puppy_name()
|
|
262
|
-
owner_name = get_owner_name()
|
|
263
|
-
model = get_active_model()
|
|
264
|
-
yolo_mode = get_yolo_mode()
|
|
265
|
-
protected_tokens = get_protected_token_count()
|
|
266
|
-
compaction_threshold = get_compaction_threshold()
|
|
267
|
-
compaction_strategy = get_compaction_strategy()
|
|
268
|
-
|
|
269
|
-
# Get current agent info
|
|
270
|
-
current_agent = get_current_agent()
|
|
271
|
-
|
|
272
|
-
status_msg = f"""[bold magenta]🐶 Puppy Status[/bold magenta]
|
|
273
|
-
|
|
274
|
-
[bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan]
|
|
275
|
-
[bold]owner_name:[/bold] [cyan]{owner_name}[/cyan]
|
|
276
|
-
[bold]current_agent:[/bold] [magenta]{current_agent.display_name}[/magenta]
|
|
277
|
-
[bold]model:[/bold] [green]{model}[/green]
|
|
278
|
-
[bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
|
|
279
|
-
[bold]DBOS:[/bold] {"[green]enabled[/green]" if get_use_dbos() else "[yellow]disabled[/yellow]"} (toggle: /set enable_dbos true|false)
|
|
280
|
-
[bold]protected_tokens:[/bold] [cyan]{protected_tokens:,}[/cyan] recent tokens preserved
|
|
281
|
-
[bold]compaction_threshold:[/bold] [cyan]{compaction_threshold:.1%}[/cyan] context usage triggers compaction
|
|
282
|
-
[bold]compaction_strategy:[/bold] [cyan]{compaction_strategy}[/cyan] (summarization or truncation)
|
|
283
|
-
[bold]reasoning_effort:[/bold] [cyan]{get_openai_reasoning_effort()}[/cyan]
|
|
284
|
-
|
|
285
|
-
"""
|
|
286
|
-
emit_info(status_msg)
|
|
287
|
-
return True
|
|
288
|
-
|
|
289
|
-
if command.startswith("/reasoning"):
|
|
290
|
-
tokens = command.split()
|
|
291
|
-
if len(tokens) != 2:
|
|
292
|
-
emit_warning("Usage: /reasoning <low|medium|high>")
|
|
293
|
-
return True
|
|
294
|
-
|
|
295
|
-
effort = tokens[1]
|
|
296
|
-
try:
|
|
297
|
-
from code_puppy.config import set_openai_reasoning_effort
|
|
298
|
-
|
|
299
|
-
set_openai_reasoning_effort(effort)
|
|
300
|
-
except ValueError as exc:
|
|
301
|
-
emit_error(str(exc))
|
|
302
|
-
return True
|
|
303
|
-
|
|
304
|
-
from code_puppy.config import get_openai_reasoning_effort
|
|
305
|
-
|
|
306
|
-
normalized_effort = get_openai_reasoning_effort()
|
|
307
|
-
|
|
308
|
-
from code_puppy.agents.agent_manager import get_current_agent
|
|
309
|
-
|
|
310
|
-
agent = get_current_agent()
|
|
311
|
-
agent.reload_code_generation_agent()
|
|
312
|
-
emit_success(
|
|
313
|
-
f"Reasoning effort set to '{normalized_effort}' and active agent reloaded"
|
|
314
|
-
)
|
|
315
|
-
return True
|
|
316
|
-
|
|
317
|
-
if command.startswith("/session"):
|
|
318
|
-
# /session id -> show current autosave id
|
|
319
|
-
# /session new -> rotate autosave id
|
|
320
|
-
tokens = command.split()
|
|
321
|
-
from code_puppy.config import (
|
|
322
|
-
AUTOSAVE_DIR,
|
|
323
|
-
get_current_autosave_id,
|
|
324
|
-
get_current_autosave_session_name,
|
|
325
|
-
rotate_autosave_id,
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
if len(tokens) == 1 or tokens[1] == "id":
|
|
329
|
-
sid = get_current_autosave_id()
|
|
330
|
-
emit_info(
|
|
331
|
-
f"[bold magenta]Autosave Session[/bold magenta]: {sid}\n"
|
|
332
|
-
f"Files prefix: {Path(AUTOSAVE_DIR) / get_current_autosave_session_name()}"
|
|
333
|
-
)
|
|
334
|
-
return True
|
|
335
|
-
if tokens[1] == "new":
|
|
336
|
-
new_sid = rotate_autosave_id()
|
|
337
|
-
emit_success(f"New autosave session id: {new_sid}")
|
|
338
|
-
return True
|
|
339
|
-
emit_warning("Usage: /session [id|new]")
|
|
340
|
-
return True
|
|
341
|
-
|
|
342
|
-
if command.startswith("/set"):
|
|
343
|
-
# Syntax: /set KEY=VALUE or /set KEY VALUE
|
|
344
|
-
from code_puppy.config import set_config_value
|
|
345
|
-
|
|
346
|
-
tokens = command.split(None, 2)
|
|
347
|
-
argstr = command[len("/set") :].strip()
|
|
348
|
-
key = None
|
|
349
|
-
value = None
|
|
350
|
-
if "=" in argstr:
|
|
351
|
-
key, value = argstr.split("=", 1)
|
|
352
|
-
key = key.strip()
|
|
353
|
-
value = value.strip()
|
|
354
|
-
elif len(tokens) >= 3:
|
|
355
|
-
key = tokens[1]
|
|
356
|
-
value = tokens[2]
|
|
357
|
-
elif len(tokens) == 2:
|
|
358
|
-
key = tokens[1]
|
|
359
|
-
value = ""
|
|
360
|
-
else:
|
|
361
|
-
config_keys = get_config_keys()
|
|
362
|
-
if "compaction_strategy" not in config_keys:
|
|
363
|
-
config_keys.append("compaction_strategy")
|
|
364
|
-
session_help = (
|
|
365
|
-
"\n[yellow]Session Management[/yellow]"
|
|
366
|
-
"\n [cyan]auto_save_session[/cyan] Auto-save chat after every response (true/false)"
|
|
367
|
-
)
|
|
368
|
-
emit_warning(
|
|
369
|
-
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]{session_help}"
|
|
370
|
-
)
|
|
371
|
-
return True
|
|
372
|
-
if key:
|
|
373
|
-
# Check if we're toggling DBOS enablement
|
|
374
|
-
if key == "enable_dbos":
|
|
375
|
-
emit_info(
|
|
376
|
-
"[yellow]⚠️ DBOS configuration changed. Please restart Code Puppy for this change to take effect.[/yellow]"
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
set_config_value(key, value)
|
|
380
|
-
emit_success(f'Set {key} = "{value}" in puppy.cfg!')
|
|
381
|
-
else:
|
|
382
|
-
emit_error("You must supply a key.")
|
|
383
|
-
return True
|
|
384
|
-
|
|
385
|
-
if command.startswith("/tools"):
|
|
386
|
-
# Display the tools_content.py file content with markdown formatting
|
|
387
|
-
from rich.markdown import Markdown
|
|
388
|
-
|
|
389
|
-
markdown_content = Markdown(tools_content)
|
|
390
|
-
emit_info(markdown_content)
|
|
391
|
-
return True
|
|
392
|
-
|
|
393
|
-
if command.startswith("/agent"):
|
|
394
|
-
# Handle agent switching
|
|
395
|
-
from code_puppy.agents import (
|
|
396
|
-
get_agent_descriptions,
|
|
397
|
-
get_available_agents,
|
|
398
|
-
get_current_agent,
|
|
399
|
-
set_current_agent,
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
tokens = command.split()
|
|
403
|
-
|
|
404
|
-
if len(tokens) == 1:
|
|
405
|
-
# Show current agent and available agents
|
|
406
|
-
current_agent = get_current_agent()
|
|
407
|
-
available_agents = get_available_agents()
|
|
408
|
-
descriptions = get_agent_descriptions()
|
|
409
|
-
|
|
410
|
-
# Generate a group ID for all messages in this command
|
|
411
|
-
import uuid
|
|
412
|
-
|
|
413
|
-
group_id = str(uuid.uuid4())
|
|
414
|
-
|
|
415
|
-
emit_info(
|
|
416
|
-
f"[bold green]Current Agent:[/bold green] {current_agent.display_name}",
|
|
417
|
-
message_group=group_id,
|
|
418
|
-
)
|
|
419
|
-
emit_info(
|
|
420
|
-
f"[dim]{current_agent.description}[/dim]\n", message_group=group_id
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
emit_info(
|
|
424
|
-
"[bold magenta]Available Agents:[/bold magenta]", message_group=group_id
|
|
425
|
-
)
|
|
426
|
-
for name, display_name in available_agents.items():
|
|
427
|
-
description = descriptions.get(name, "No description")
|
|
428
|
-
current_marker = (
|
|
429
|
-
" [green]← current[/green]" if name == current_agent.name else ""
|
|
430
|
-
)
|
|
431
|
-
emit_info(
|
|
432
|
-
f" [cyan]{name:<12}[/cyan] {display_name}{current_marker}",
|
|
433
|
-
message_group=group_id,
|
|
434
|
-
)
|
|
435
|
-
emit_info(f" [dim]{description}[/dim]", message_group=group_id)
|
|
436
|
-
|
|
437
|
-
emit_info(
|
|
438
|
-
"\n[yellow]Usage:[/yellow] /agent <agent-name>", message_group=group_id
|
|
439
|
-
)
|
|
440
|
-
return True
|
|
441
|
-
|
|
442
|
-
elif len(tokens) == 2:
|
|
443
|
-
agent_name = tokens[1].lower()
|
|
444
|
-
|
|
445
|
-
# Generate a group ID for all messages in this command
|
|
446
|
-
import uuid
|
|
447
|
-
|
|
448
|
-
group_id = str(uuid.uuid4())
|
|
449
|
-
available_agents = get_available_agents()
|
|
450
|
-
|
|
451
|
-
if agent_name not in available_agents:
|
|
452
|
-
emit_error(f"Agent '{agent_name}' not found", message_group=group_id)
|
|
453
|
-
emit_warning(
|
|
454
|
-
f"Available agents: {', '.join(available_agents.keys())}",
|
|
455
|
-
message_group=group_id,
|
|
456
|
-
)
|
|
457
|
-
return True
|
|
458
|
-
|
|
459
|
-
current_agent = get_current_agent()
|
|
460
|
-
if current_agent.name == agent_name:
|
|
461
|
-
emit_info(
|
|
462
|
-
f"Already using agent: {current_agent.display_name}",
|
|
463
|
-
message_group=group_id,
|
|
464
|
-
)
|
|
465
|
-
return True
|
|
466
|
-
|
|
467
|
-
new_session_id = finalize_autosave_session()
|
|
468
|
-
if not set_current_agent(agent_name):
|
|
469
|
-
emit_warning(
|
|
470
|
-
"Agent switch failed after autosave rotation. Your context was preserved.",
|
|
471
|
-
message_group=group_id,
|
|
472
|
-
)
|
|
473
|
-
return True
|
|
474
|
-
|
|
475
|
-
new_agent = get_current_agent()
|
|
476
|
-
new_agent.reload_code_generation_agent()
|
|
477
|
-
emit_success(
|
|
478
|
-
f"Switched to agent: {new_agent.display_name}",
|
|
479
|
-
message_group=group_id,
|
|
480
|
-
)
|
|
481
|
-
emit_info(f"[dim]{new_agent.description}[/dim]", message_group=group_id)
|
|
482
|
-
emit_info(
|
|
483
|
-
f"[dim]Auto-save session rotated to: {new_session_id}[/dim]",
|
|
484
|
-
message_group=group_id,
|
|
485
|
-
)
|
|
486
|
-
return True
|
|
487
|
-
else:
|
|
488
|
-
emit_warning("Usage: /agent [agent-name]")
|
|
489
|
-
return True
|
|
490
|
-
|
|
491
|
-
if command.startswith("/model") or command.startswith("/m "):
|
|
492
|
-
# Try setting model and show confirmation
|
|
493
|
-
# Handle both /model and /m for backward compatibility
|
|
494
|
-
model_command = command
|
|
495
|
-
if command.startswith("/model"):
|
|
496
|
-
# Convert /model to /m for internal processing
|
|
497
|
-
model_command = command.replace("/model", "/m", 1)
|
|
498
|
-
|
|
499
|
-
# If no model matched, show available models
|
|
500
|
-
from code_puppy.command_line.model_picker_completion import load_model_names
|
|
501
|
-
|
|
502
|
-
new_input = update_model_in_input(model_command)
|
|
503
|
-
if new_input is not None:
|
|
504
|
-
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
505
|
-
|
|
506
|
-
model = get_active_model()
|
|
507
|
-
# Make sure this is called for the test
|
|
508
|
-
emit_success(f"Active model set and loaded: {model}")
|
|
509
|
-
return True
|
|
510
|
-
model_names = load_model_names()
|
|
511
|
-
emit_warning("Usage: /model <model-name> or /m <model-name>")
|
|
512
|
-
emit_warning(f"Available models: {', '.join(model_names)}")
|
|
513
|
-
return True
|
|
514
|
-
|
|
515
|
-
if command.startswith("/mcp"):
|
|
516
|
-
from code_puppy.command_line.mcp import MCPCommandHandler
|
|
517
|
-
|
|
518
|
-
handler = MCPCommandHandler()
|
|
519
|
-
return handler.handle_mcp_command(command)
|
|
520
|
-
|
|
521
|
-
# Built-in help
|
|
522
|
-
if command in ("/help", "/h"):
|
|
523
|
-
import uuid
|
|
524
|
-
|
|
525
|
-
group_id = str(uuid.uuid4())
|
|
526
|
-
help_text = get_commands_help()
|
|
527
|
-
emit_info(help_text, message_group_id=group_id)
|
|
528
|
-
return True
|
|
529
|
-
|
|
530
|
-
if command.startswith("/pin_model"):
|
|
531
|
-
# Handle agent model pinning
|
|
532
|
-
import json
|
|
533
|
-
|
|
534
|
-
from code_puppy.agents.json_agent import discover_json_agents
|
|
535
|
-
from code_puppy.command_line.model_picker_completion import load_model_names
|
|
536
|
-
|
|
537
|
-
tokens = command.split()
|
|
538
|
-
|
|
539
|
-
if len(tokens) != 3:
|
|
540
|
-
emit_warning("Usage: /pin_model <agent-name> <model-name>")
|
|
541
|
-
|
|
542
|
-
# Show available models and agents
|
|
543
|
-
available_models = load_model_names()
|
|
544
|
-
json_agents = discover_json_agents()
|
|
545
|
-
|
|
546
|
-
# Get built-in agents
|
|
547
|
-
from code_puppy.agents.agent_manager import get_agent_descriptions
|
|
548
|
-
|
|
549
|
-
builtin_agents = get_agent_descriptions()
|
|
550
|
-
|
|
551
|
-
emit_info("Available models:")
|
|
552
|
-
for model in available_models:
|
|
553
|
-
emit_info(f" [cyan]{model}[/cyan]")
|
|
554
|
-
|
|
555
|
-
if builtin_agents:
|
|
556
|
-
emit_info("\nAvailable built-in agents:")
|
|
557
|
-
for agent_name, description in builtin_agents.items():
|
|
558
|
-
emit_info(f" [cyan]{agent_name}[/cyan] - {description}")
|
|
559
|
-
|
|
560
|
-
if json_agents:
|
|
561
|
-
emit_info("\nAvailable JSON agents:")
|
|
562
|
-
for agent_name, agent_path in json_agents.items():
|
|
563
|
-
emit_info(f" [cyan]{agent_name}[/cyan] ({agent_path})")
|
|
564
|
-
return True
|
|
565
|
-
|
|
566
|
-
agent_name = tokens[1].lower()
|
|
567
|
-
model_name = tokens[2]
|
|
568
|
-
|
|
569
|
-
# Check if model exists
|
|
570
|
-
available_models = load_model_names()
|
|
571
|
-
if model_name not in available_models:
|
|
572
|
-
emit_error(f"Model '{model_name}' not found")
|
|
573
|
-
emit_warning(f"Available models: {', '.join(available_models)}")
|
|
574
|
-
return True
|
|
575
|
-
|
|
576
|
-
# Check if this is a JSON agent or a built-in Python agent
|
|
577
|
-
json_agents = discover_json_agents()
|
|
578
|
-
|
|
579
|
-
# Get list of available built-in agents
|
|
580
|
-
from code_puppy.agents.agent_manager import get_agent_descriptions
|
|
581
|
-
|
|
582
|
-
builtin_agents = get_agent_descriptions()
|
|
583
|
-
|
|
584
|
-
is_json_agent = agent_name in json_agents
|
|
585
|
-
is_builtin_agent = agent_name in builtin_agents
|
|
586
|
-
|
|
587
|
-
if not is_json_agent and not is_builtin_agent:
|
|
588
|
-
emit_error(f"Agent '{agent_name}' not found")
|
|
589
|
-
|
|
590
|
-
# Show available agents
|
|
591
|
-
if builtin_agents:
|
|
592
|
-
emit_info("Available built-in agents:")
|
|
593
|
-
for name, desc in builtin_agents.items():
|
|
594
|
-
emit_info(f" [cyan]{name}[/cyan] - {desc}")
|
|
595
|
-
|
|
596
|
-
if json_agents:
|
|
597
|
-
emit_info("\nAvailable JSON agents:")
|
|
598
|
-
for name, path in json_agents.items():
|
|
599
|
-
emit_info(f" [cyan]{name}[/cyan] ({path})")
|
|
600
|
-
return True
|
|
601
|
-
|
|
602
|
-
# Handle different agent types
|
|
603
|
-
try:
|
|
604
|
-
if is_json_agent:
|
|
605
|
-
# Handle JSON agent - modify the JSON file
|
|
606
|
-
agent_file_path = json_agents[agent_name]
|
|
607
|
-
|
|
608
|
-
with open(agent_file_path, "r", encoding="utf-8") as f:
|
|
609
|
-
agent_config = json.load(f)
|
|
610
|
-
|
|
611
|
-
# Set the model
|
|
612
|
-
agent_config["model"] = model_name
|
|
613
|
-
|
|
614
|
-
# Save the updated configuration
|
|
615
|
-
with open(agent_file_path, "w", encoding="utf-8") as f:
|
|
616
|
-
json.dump(agent_config, f, indent=2, ensure_ascii=False)
|
|
617
|
-
|
|
618
|
-
else:
|
|
619
|
-
# Handle built-in Python agent - store in config
|
|
620
|
-
from code_puppy.config import set_agent_pinned_model
|
|
621
|
-
|
|
622
|
-
set_agent_pinned_model(agent_name, model_name)
|
|
623
|
-
|
|
624
|
-
emit_success(f"Model '{model_name}' pinned to agent '{agent_name}'")
|
|
625
|
-
|
|
626
|
-
# If this is the current agent, refresh it so the prompt updates immediately
|
|
627
|
-
from code_puppy.agents import get_current_agent
|
|
628
|
-
|
|
629
|
-
current_agent = get_current_agent()
|
|
630
|
-
if current_agent.name == agent_name:
|
|
631
|
-
try:
|
|
632
|
-
if is_json_agent and hasattr(current_agent, "refresh_config"):
|
|
633
|
-
current_agent.refresh_config()
|
|
634
|
-
current_agent.reload_code_generation_agent()
|
|
635
|
-
emit_info(f"Active agent reloaded with pinned model '{model_name}'")
|
|
636
|
-
except Exception as reload_error:
|
|
637
|
-
emit_warning(
|
|
638
|
-
f"Pinned model applied but reload failed: {reload_error}"
|
|
639
|
-
)
|
|
640
|
-
|
|
641
|
-
return True
|
|
642
|
-
|
|
643
|
-
except Exception as e:
|
|
644
|
-
emit_error(f"Failed to pin model to agent '{agent_name}': {e}")
|
|
645
|
-
return True
|
|
646
|
-
|
|
647
|
-
if command.startswith("/generate-pr-description"):
|
|
648
|
-
# Parse directory argument (e.g., /generate-pr-description @some/dir)
|
|
649
|
-
tokens = command.split()
|
|
650
|
-
directory_context = ""
|
|
651
|
-
for t in tokens:
|
|
652
|
-
if t.startswith("@"):
|
|
653
|
-
directory_context = f" Please work in the directory: {t[1:]}"
|
|
654
|
-
break
|
|
655
|
-
|
|
656
|
-
# Hard-coded prompt from user requirements
|
|
657
|
-
pr_prompt = f"""Generate a comprehensive PR description for my current branch changes. Follow these steps:
|
|
658
|
-
|
|
659
|
-
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.
|
|
660
|
-
2 Analyze the code: Read and analyze all modified files to understand:
|
|
661
|
-
• What functionality was added/changed/removed
|
|
662
|
-
• The technical approach and implementation details
|
|
663
|
-
• Any architectural or design pattern changes
|
|
664
|
-
• Dependencies added/removed/updated
|
|
665
|
-
3 Generate a structured PR description with these sections:
|
|
666
|
-
• Title: Concise, descriptive title (50 chars max)
|
|
667
|
-
• Summary: Brief overview of what this PR accomplishes
|
|
668
|
-
• Changes Made: Detailed bullet points of specific changes
|
|
669
|
-
• Technical Details: Implementation approach, design decisions, patterns used
|
|
670
|
-
• Files Modified: List of key files with brief description of changes
|
|
671
|
-
• Testing: What was tested and how (if applicable)
|
|
672
|
-
• Breaking Changes: Any breaking changes (if applicable)
|
|
673
|
-
• Additional Notes: Any other relevant information
|
|
674
|
-
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
|
|
675
|
-
description field. Use proper markdown syntax with headers, bullet points, code blocks, and formatting.
|
|
676
|
-
5 Make it review-ready: Ensure the description helps reviewers understand the context, approach, and impact of the changes.
|
|
677
|
-
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}"""
|
|
678
|
-
|
|
679
|
-
# Return the prompt to be processed by the main chat system
|
|
680
|
-
return pr_prompt
|
|
681
|
-
|
|
682
|
-
if command.startswith("/dump_context"):
|
|
683
|
-
from code_puppy.agents.agent_manager import get_current_agent
|
|
684
|
-
|
|
685
|
-
tokens = command.split()
|
|
686
|
-
if len(tokens) != 2:
|
|
687
|
-
emit_warning("Usage: /dump_context <session_name>")
|
|
688
|
-
return True
|
|
689
|
-
|
|
690
|
-
session_name = tokens[1]
|
|
691
|
-
agent = get_current_agent()
|
|
692
|
-
history = agent.get_message_history()
|
|
693
|
-
|
|
694
|
-
if not history:
|
|
695
|
-
emit_warning("No message history to dump!")
|
|
696
|
-
return True
|
|
697
|
-
|
|
698
|
-
try:
|
|
699
|
-
metadata = save_session(
|
|
700
|
-
history=history,
|
|
701
|
-
session_name=session_name,
|
|
702
|
-
base_dir=Path(CONTEXTS_DIR),
|
|
703
|
-
timestamp=datetime.now().isoformat(),
|
|
704
|
-
token_estimator=agent.estimate_tokens_for_message,
|
|
705
|
-
)
|
|
706
|
-
emit_success(
|
|
707
|
-
f"✅ Context saved: {metadata.message_count} messages ({metadata.total_tokens} tokens)\n"
|
|
708
|
-
f"📁 Files: {metadata.pickle_path}, {metadata.metadata_path}"
|
|
709
|
-
)
|
|
710
|
-
return True
|
|
711
|
-
|
|
712
|
-
except Exception as exc:
|
|
713
|
-
emit_error(f"Failed to dump context: {exc}")
|
|
714
|
-
return True
|
|
715
|
-
|
|
716
|
-
if command.startswith("/load_context"):
|
|
717
|
-
from code_puppy.agents.agent_manager import get_current_agent
|
|
718
|
-
|
|
719
|
-
tokens = command.split()
|
|
720
|
-
if len(tokens) != 2:
|
|
721
|
-
emit_warning("Usage: /load_context <session_name>")
|
|
722
|
-
return True
|
|
723
|
-
|
|
724
|
-
session_name = tokens[1]
|
|
725
|
-
contexts_dir = Path(CONTEXTS_DIR)
|
|
726
|
-
session_path = contexts_dir / f"{session_name}.pkl"
|
|
727
|
-
|
|
728
|
-
try:
|
|
729
|
-
history = load_session(session_name, contexts_dir)
|
|
730
|
-
except FileNotFoundError:
|
|
731
|
-
emit_error(f"Context file not found: {session_path}")
|
|
732
|
-
available = list_sessions(contexts_dir)
|
|
733
|
-
if available:
|
|
734
|
-
emit_info(f"Available contexts: {', '.join(available)}")
|
|
735
|
-
return True
|
|
736
|
-
except Exception as exc:
|
|
737
|
-
emit_error(f"Failed to load context: {exc}")
|
|
738
|
-
return True
|
|
739
|
-
|
|
740
|
-
agent = get_current_agent()
|
|
741
|
-
agent.set_message_history(history)
|
|
742
|
-
total_tokens = sum(agent.estimate_tokens_for_message(m) for m in history)
|
|
743
|
-
|
|
744
|
-
# Rotate autosave id to avoid overwriting any existing autosave
|
|
745
|
-
try:
|
|
746
|
-
from code_puppy.config import rotate_autosave_id
|
|
747
|
-
|
|
748
|
-
new_id = rotate_autosave_id()
|
|
749
|
-
autosave_info = f"\n[dim]Autosave session rotated to: {new_id}[/dim]"
|
|
750
|
-
except Exception:
|
|
751
|
-
autosave_info = ""
|
|
752
|
-
|
|
753
|
-
emit_success(
|
|
754
|
-
f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
|
755
|
-
f"📁 From: {session_path}{autosave_info}"
|
|
756
|
-
)
|
|
757
|
-
return True
|
|
758
|
-
|
|
759
|
-
if command.startswith("/truncate"):
|
|
760
|
-
from code_puppy.agents.agent_manager import get_current_agent
|
|
761
|
-
|
|
762
|
-
tokens = command.split()
|
|
763
|
-
if len(tokens) != 2:
|
|
764
|
-
emit_error(
|
|
765
|
-
"Usage: /truncate <N> (where N is the number of messages to keep)"
|
|
766
|
-
)
|
|
767
|
-
return True
|
|
171
|
+
from rich.text import Text
|
|
768
172
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
if n < 1:
|
|
772
|
-
emit_error("N must be a positive integer")
|
|
773
|
-
return True
|
|
774
|
-
except ValueError:
|
|
775
|
-
emit_error("N must be a valid integer")
|
|
776
|
-
return True
|
|
777
|
-
|
|
778
|
-
agent = get_current_agent()
|
|
779
|
-
history = agent.get_message_history()
|
|
780
|
-
if not history:
|
|
781
|
-
emit_warning("No history to truncate yet. Ask me something first!")
|
|
782
|
-
return True
|
|
783
|
-
|
|
784
|
-
if len(history) <= n:
|
|
785
|
-
emit_info(
|
|
786
|
-
f"History already has {len(history)} messages, which is <= {n}. Nothing to truncate."
|
|
787
|
-
)
|
|
788
|
-
return True
|
|
173
|
+
from code_puppy.command_line.command_registry import get_command
|
|
174
|
+
from code_puppy.messaging import emit_info, emit_warning
|
|
789
175
|
|
|
790
|
-
|
|
791
|
-
truncated_history = (
|
|
792
|
-
[history[0]] + history[-(n - 1) :] if n > 1 else [history[0]]
|
|
793
|
-
)
|
|
176
|
+
_ensure_plugins_loaded()
|
|
794
177
|
|
|
795
|
-
|
|
796
|
-
emit_success(
|
|
797
|
-
f"Truncated message history from {len(history)} to {len(truncated_history)} messages (keeping system message and {n - 1} most recent)"
|
|
798
|
-
)
|
|
799
|
-
return True
|
|
178
|
+
command = command.strip()
|
|
800
179
|
|
|
801
|
-
if
|
|
802
|
-
|
|
803
|
-
#
|
|
804
|
-
|
|
805
|
-
|
|
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.
|
|
806
227
|
|
|
807
228
|
# Try plugin-provided custom commands before unknown warning
|
|
808
229
|
if command.startswith("/"):
|
|
@@ -811,11 +232,24 @@ def handle_command(command: str):
|
|
|
811
232
|
try:
|
|
812
233
|
from code_puppy import callbacks
|
|
813
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
|
+
|
|
814
243
|
results = callbacks.on_custom_command(command=command, name=name)
|
|
815
244
|
# Iterate through callback results; treat str as handled (no model run)
|
|
816
245
|
for res in results:
|
|
817
246
|
if res is True:
|
|
818
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
|
|
819
253
|
if isinstance(res, str):
|
|
820
254
|
# Display returned text to the user and treat as handled
|
|
821
255
|
try:
|
|
@@ -829,7 +263,9 @@ def handle_command(command: str):
|
|
|
829
263
|
|
|
830
264
|
if name:
|
|
831
265
|
emit_warning(
|
|
832
|
-
|
|
266
|
+
Text.from_markup(
|
|
267
|
+
f"Unknown command: {command}\n[dim]Type /help for options.[/dim]"
|
|
268
|
+
)
|
|
833
269
|
)
|
|
834
270
|
else:
|
|
835
271
|
# Show current model ONLY here
|
|
@@ -837,7 +273,9 @@ def handle_command(command: str):
|
|
|
837
273
|
|
|
838
274
|
current_model = get_active_model()
|
|
839
275
|
emit_info(
|
|
840
|
-
|
|
276
|
+
Text.from_markup(
|
|
277
|
+
f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
|
|
278
|
+
)
|
|
841
279
|
)
|
|
842
280
|
return True
|
|
843
281
|
|