newcode 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +147 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +630 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +122 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +380 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +167 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2145 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +296 -0
- code_puppy/agents/pack/husky.py +307 -0
- code_puppy/agents/pack/retriever.py +380 -0
- code_puppy/agents/pack/shepherd.py +327 -0
- code_puppy/agents/pack/terrier.py +281 -0
- code_puppy/agents/pack/watchdog.py +357 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +674 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +664 -0
- code_puppy/cli_runner.py +1038 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +526 -0
- code_puppy/command_line/command_handler.py +283 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +853 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +91 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1787 -0
- code_puppy/error_logging.py +133 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +15 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +195 -0
- code_puppy/hook_engine/executor.py +293 -0
- code_puppy/hook_engine/matcher.py +145 -0
- code_puppy/hook_engine/models.py +222 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +141 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1153 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +96 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +130 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +100 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +295 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +499 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +131 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +163 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +601 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +48 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +528 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +277 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +551 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +205 -0
- code_puppy/plugins/oauth_puppy_html.py +224 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +317 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +470 -0
- code_puppy/tools/agent_tools.py +616 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +36 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +739 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +251 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- newcode-0.1.1.data/data/code_puppy/models.json +130 -0
- newcode-0.1.1.data/data/code_puppy/models_dev_api.json +1 -0
- newcode-0.1.1.dist-info/METADATA +154 -0
- newcode-0.1.1.dist-info/RECORD +289 -0
- newcode-0.1.1.dist-info/WHEEL +4 -0
- newcode-0.1.1.dist-info/entry_points.txt +3 -0
- newcode-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""TUI-based wizard for creating scheduled tasks.
|
|
2
|
+
|
|
3
|
+
Provides interactive menus with arrow-key navigation for selecting
|
|
4
|
+
schedule type, agent, model, and other task parameters.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from prompt_toolkit.application import Application
|
|
10
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
11
|
+
from prompt_toolkit.layout import Layout, Window
|
|
12
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
13
|
+
|
|
14
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SelectionMenu:
|
|
18
|
+
"""Simple arrow-key selection menu."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self, title: str, choices: List[str], descriptions: Optional[List[str]] = None
|
|
22
|
+
):
|
|
23
|
+
self.title = title
|
|
24
|
+
self.choices = choices
|
|
25
|
+
self.descriptions = descriptions or [""] * len(choices)
|
|
26
|
+
self.selected_idx = 0
|
|
27
|
+
self.result: Optional[str] = None
|
|
28
|
+
self.cancelled = False
|
|
29
|
+
|
|
30
|
+
def _render(self) -> List:
|
|
31
|
+
"""Render the menu."""
|
|
32
|
+
lines = []
|
|
33
|
+
|
|
34
|
+
# Title
|
|
35
|
+
lines.append(("bold fg:ansicyan", f"\n {self.title}\n\n"))
|
|
36
|
+
|
|
37
|
+
# Choices
|
|
38
|
+
for i, choice in enumerate(self.choices):
|
|
39
|
+
is_selected = i == self.selected_idx
|
|
40
|
+
prefix = " ❯ " if is_selected else " "
|
|
41
|
+
|
|
42
|
+
if is_selected:
|
|
43
|
+
lines.append(("bold fg:ansigreen", prefix))
|
|
44
|
+
lines.append(("bold fg:ansigreen", f"{choice}"))
|
|
45
|
+
else:
|
|
46
|
+
lines.append(("", prefix))
|
|
47
|
+
lines.append(("fg:ansibrightblack", f"{choice}"))
|
|
48
|
+
|
|
49
|
+
# Show description for selected item
|
|
50
|
+
if is_selected and self.descriptions[i]:
|
|
51
|
+
lines.append(("fg:ansibrightblack", f" - {self.descriptions[i]}"))
|
|
52
|
+
|
|
53
|
+
lines.append(("", "\n"))
|
|
54
|
+
|
|
55
|
+
# Help text
|
|
56
|
+
lines.append(("", "\n"))
|
|
57
|
+
lines.append(("fg:ansibrightblack", " ↑/↓ Navigate "))
|
|
58
|
+
lines.append(("fg:ansigreen", "Enter "))
|
|
59
|
+
lines.append(("fg:ansibrightblack", "Select "))
|
|
60
|
+
lines.append(("fg:ansired", "Ctrl+C "))
|
|
61
|
+
lines.append(("fg:ansibrightblack", "Cancel"))
|
|
62
|
+
|
|
63
|
+
return lines
|
|
64
|
+
|
|
65
|
+
def run(self) -> Optional[str]:
|
|
66
|
+
"""Run the selection menu. Returns selected choice or None if cancelled."""
|
|
67
|
+
control = FormattedTextControl(text="")
|
|
68
|
+
window = Window(content=control, wrap_lines=True)
|
|
69
|
+
|
|
70
|
+
kb = KeyBindings()
|
|
71
|
+
|
|
72
|
+
@kb.add("up")
|
|
73
|
+
@kb.add("k")
|
|
74
|
+
def _(event):
|
|
75
|
+
if self.selected_idx > 0:
|
|
76
|
+
self.selected_idx -= 1
|
|
77
|
+
control.text = self._render()
|
|
78
|
+
|
|
79
|
+
@kb.add("down")
|
|
80
|
+
@kb.add("j")
|
|
81
|
+
def _(event):
|
|
82
|
+
if self.selected_idx < len(self.choices) - 1:
|
|
83
|
+
self.selected_idx += 1
|
|
84
|
+
control.text = self._render()
|
|
85
|
+
|
|
86
|
+
@kb.add("enter")
|
|
87
|
+
def _(event):
|
|
88
|
+
self.result = self.choices[self.selected_idx]
|
|
89
|
+
event.app.exit()
|
|
90
|
+
|
|
91
|
+
@kb.add("c-c")
|
|
92
|
+
@kb.add("escape")
|
|
93
|
+
def _(event):
|
|
94
|
+
self.cancelled = True
|
|
95
|
+
event.app.exit()
|
|
96
|
+
|
|
97
|
+
layout = Layout(window)
|
|
98
|
+
app = Application(layout=layout, key_bindings=kb, full_screen=False)
|
|
99
|
+
|
|
100
|
+
set_awaiting_user_input(True)
|
|
101
|
+
try:
|
|
102
|
+
control.text = self._render()
|
|
103
|
+
app.run(in_thread=True)
|
|
104
|
+
finally:
|
|
105
|
+
set_awaiting_user_input(False)
|
|
106
|
+
|
|
107
|
+
if self.cancelled:
|
|
108
|
+
return None
|
|
109
|
+
return self.result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TextInputMenu:
|
|
113
|
+
"""Simple text input with TUI styling."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, title: str, default: str = "", placeholder: str = ""):
|
|
116
|
+
self.title = title
|
|
117
|
+
self.default = default
|
|
118
|
+
self.placeholder = placeholder
|
|
119
|
+
|
|
120
|
+
def run(self) -> Optional[str]:
|
|
121
|
+
"""Run text input. Returns entered text or None if cancelled."""
|
|
122
|
+
from code_puppy.command_line.utils import safe_input
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
prompt = f" {self.title}"
|
|
126
|
+
if self.default:
|
|
127
|
+
prompt += f" [{self.default}]"
|
|
128
|
+
prompt += ": "
|
|
129
|
+
|
|
130
|
+
value = safe_input(prompt).strip()
|
|
131
|
+
if not value and self.default:
|
|
132
|
+
return self.default
|
|
133
|
+
return value if value else None
|
|
134
|
+
except (KeyboardInterrupt, EOFError):
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class MultilineInputMenu:
|
|
139
|
+
"""Multi-line text input for prompts."""
|
|
140
|
+
|
|
141
|
+
def __init__(self, title: str):
|
|
142
|
+
self.title = title
|
|
143
|
+
|
|
144
|
+
def run(self) -> Optional[str]:
|
|
145
|
+
"""Run multiline input. Returns entered text or None if cancelled."""
|
|
146
|
+
from code_puppy.command_line.utils import safe_input
|
|
147
|
+
|
|
148
|
+
print(f"\n {self.title}")
|
|
149
|
+
print(" (Enter an empty line to finish, Ctrl+C to cancel)\n")
|
|
150
|
+
|
|
151
|
+
lines = []
|
|
152
|
+
try:
|
|
153
|
+
while True:
|
|
154
|
+
line = safe_input(" > ")
|
|
155
|
+
if not line:
|
|
156
|
+
break
|
|
157
|
+
lines.append(line)
|
|
158
|
+
except (KeyboardInterrupt, EOFError):
|
|
159
|
+
print("\n Cancelled.")
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
return "\n".join(lines) if lines else None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_available_agents_list() -> List[Tuple[str, str]]:
|
|
166
|
+
"""Get list of available agents with descriptions."""
|
|
167
|
+
try:
|
|
168
|
+
from code_puppy.agents import get_agent_descriptions, get_available_agents
|
|
169
|
+
|
|
170
|
+
agents = get_available_agents()
|
|
171
|
+
descriptions = get_agent_descriptions()
|
|
172
|
+
|
|
173
|
+
result = []
|
|
174
|
+
for agent_name in sorted(agents.keys()):
|
|
175
|
+
desc = descriptions.get(agent_name, agents.get(agent_name, ""))
|
|
176
|
+
result.append((agent_name, desc))
|
|
177
|
+
return result
|
|
178
|
+
except Exception:
|
|
179
|
+
return [("code-puppy", "Default agent")]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_available_models_list() -> List[str]:
|
|
183
|
+
"""Get list of available models."""
|
|
184
|
+
try:
|
|
185
|
+
from code_puppy.command_line.model_picker_completion import load_model_names
|
|
186
|
+
|
|
187
|
+
models = load_model_names()
|
|
188
|
+
return models if models else ["(default)"]
|
|
189
|
+
except Exception:
|
|
190
|
+
return ["(default)"]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def create_task_wizard() -> Optional[dict]:
|
|
194
|
+
"""Run the full task creation wizard.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
dict with task parameters, or None if cancelled.
|
|
198
|
+
"""
|
|
199
|
+
print("\n" + "=" * 60)
|
|
200
|
+
print("📅 CREATE NEW SCHEDULED TASK")
|
|
201
|
+
print("=" * 60)
|
|
202
|
+
|
|
203
|
+
# Step 1: Task Name
|
|
204
|
+
name_input = TextInputMenu("Task name", placeholder="e.g., Daily Code Review")
|
|
205
|
+
task_name = name_input.run()
|
|
206
|
+
if not task_name:
|
|
207
|
+
print("\n ❌ Cancelled - task name required.")
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
# Step 2: Schedule Type
|
|
211
|
+
schedule_menu = SelectionMenu(
|
|
212
|
+
"Select schedule type:",
|
|
213
|
+
choices=[
|
|
214
|
+
"Every 15 minutes",
|
|
215
|
+
"Every 30 minutes",
|
|
216
|
+
"Every hour",
|
|
217
|
+
"Every 2 hours",
|
|
218
|
+
"Every 6 hours",
|
|
219
|
+
"Daily",
|
|
220
|
+
"Custom interval...",
|
|
221
|
+
],
|
|
222
|
+
descriptions=[
|
|
223
|
+
"Run 4 times per hour",
|
|
224
|
+
"Run twice per hour",
|
|
225
|
+
"Run once per hour",
|
|
226
|
+
"Run 12 times per day",
|
|
227
|
+
"Run 4 times per day",
|
|
228
|
+
"Run once per day",
|
|
229
|
+
"Specify custom interval like 45m, 3h, 2d",
|
|
230
|
+
],
|
|
231
|
+
)
|
|
232
|
+
schedule_choice = schedule_menu.run()
|
|
233
|
+
if not schedule_choice:
|
|
234
|
+
print("\n ❌ Cancelled.")
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
# Map choice to schedule type and value
|
|
238
|
+
schedule_map = {
|
|
239
|
+
"Every 15 minutes": ("interval", "15m"),
|
|
240
|
+
"Every 30 minutes": ("interval", "30m"),
|
|
241
|
+
"Every hour": ("hourly", "1h"),
|
|
242
|
+
"Every 2 hours": ("interval", "2h"),
|
|
243
|
+
"Every 6 hours": ("interval", "6h"),
|
|
244
|
+
"Daily": ("daily", "24h"),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if schedule_choice == "Custom interval...":
|
|
248
|
+
interval_input = TextInputMenu(
|
|
249
|
+
"Enter interval (e.g., 45m, 3h, 2d)", default="1h"
|
|
250
|
+
)
|
|
251
|
+
custom_interval = interval_input.run()
|
|
252
|
+
if not custom_interval:
|
|
253
|
+
print("\n ❌ Cancelled.")
|
|
254
|
+
return None
|
|
255
|
+
schedule_type = "interval"
|
|
256
|
+
schedule_value = custom_interval
|
|
257
|
+
else:
|
|
258
|
+
schedule_type, schedule_value = schedule_map[schedule_choice]
|
|
259
|
+
|
|
260
|
+
# Step 3: Agent Selection
|
|
261
|
+
agents = get_available_agents_list()
|
|
262
|
+
agent_names = [a[0] for a in agents]
|
|
263
|
+
agent_descs = [a[1] for a in agents]
|
|
264
|
+
|
|
265
|
+
# Put code-puppy first if it exists
|
|
266
|
+
if "code-puppy" in agent_names:
|
|
267
|
+
idx = agent_names.index("code-puppy")
|
|
268
|
+
agent_names.insert(0, agent_names.pop(idx))
|
|
269
|
+
agent_descs.insert(0, agent_descs.pop(idx))
|
|
270
|
+
|
|
271
|
+
agent_menu = SelectionMenu(
|
|
272
|
+
"Select agent:", choices=agent_names, descriptions=agent_descs
|
|
273
|
+
)
|
|
274
|
+
selected_agent = agent_menu.run()
|
|
275
|
+
if not selected_agent:
|
|
276
|
+
print("\n ❌ Cancelled.")
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
# Step 4: Model Selection
|
|
280
|
+
models = get_available_models_list()
|
|
281
|
+
models.insert(0, "(use default model)")
|
|
282
|
+
|
|
283
|
+
model_menu = SelectionMenu("Select model:", choices=models, descriptions=None)
|
|
284
|
+
selected_model = model_menu.run()
|
|
285
|
+
if selected_model is None:
|
|
286
|
+
print("\n ❌ Cancelled.")
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
if selected_model == "(use default model)":
|
|
290
|
+
selected_model = ""
|
|
291
|
+
|
|
292
|
+
# Step 5: Prompt
|
|
293
|
+
print()
|
|
294
|
+
prompt_input = MultilineInputMenu("Enter the prompt for this task:")
|
|
295
|
+
task_prompt = prompt_input.run()
|
|
296
|
+
if not task_prompt:
|
|
297
|
+
print("\n ❌ Cancelled - prompt required.")
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
# Step 6: Working Directory
|
|
301
|
+
workdir_input = TextInputMenu(
|
|
302
|
+
"Working directory", default=".", placeholder="current directory"
|
|
303
|
+
)
|
|
304
|
+
working_dir = workdir_input.run()
|
|
305
|
+
if working_dir is None:
|
|
306
|
+
print("\n ❌ Cancelled.")
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
# Summary
|
|
310
|
+
print("\n" + "-" * 60)
|
|
311
|
+
print("📋 TASK SUMMARY")
|
|
312
|
+
print("-" * 60)
|
|
313
|
+
print(f" Name: {task_name}")
|
|
314
|
+
print(f" Schedule: {schedule_type} ({schedule_value})")
|
|
315
|
+
print(f" Agent: {selected_agent}")
|
|
316
|
+
print(f" Model: {selected_model or '(default)'}")
|
|
317
|
+
print(f" Directory: {working_dir}")
|
|
318
|
+
print(f" Prompt: {task_prompt[:50]}{'...' if len(task_prompt) > 50 else ''}")
|
|
319
|
+
print("-" * 60)
|
|
320
|
+
|
|
321
|
+
# Confirm
|
|
322
|
+
from code_puppy.command_line.utils import safe_input
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
confirm = safe_input("\n Create this task? (Y/n): ").strip().lower()
|
|
326
|
+
if confirm and confirm not in ("y", "yes"):
|
|
327
|
+
print("\n ❌ Cancelled.")
|
|
328
|
+
return None
|
|
329
|
+
except (KeyboardInterrupt, EOFError):
|
|
330
|
+
print("\n ❌ Cancelled.")
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
"name": task_name,
|
|
335
|
+
"prompt": task_prompt,
|
|
336
|
+
"agent": selected_agent,
|
|
337
|
+
"model": selected_model,
|
|
338
|
+
"schedule_type": schedule_type,
|
|
339
|
+
"schedule_value": schedule_value,
|
|
340
|
+
"working_directory": working_dir,
|
|
341
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Shell command safety assessment agent.
|
|
2
|
+
|
|
3
|
+
This agent provides rapid risk assessment of shell commands before execution.
|
|
4
|
+
It's designed to be ultra-lightweight with a concise prompt (<200 tokens) and
|
|
5
|
+
uses structured output for reliable parsing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, List
|
|
9
|
+
|
|
10
|
+
from code_puppy.agents.base_agent import BaseAgent
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ShellSafetyAgent(BaseAgent):
|
|
17
|
+
"""Lightweight agent for assessing shell command safety risks.
|
|
18
|
+
|
|
19
|
+
This agent evaluates shell commands for potential risks including:
|
|
20
|
+
- File system destruction (rm -rf, dd, format, mkfs)
|
|
21
|
+
- Database operations (DROP, TRUNCATE, unfiltered UPDATE/DELETE)
|
|
22
|
+
- Privilege escalation (sudo, su, chmod 777)
|
|
23
|
+
- Network operations (wget/curl to unknown hosts)
|
|
24
|
+
- Data exfiltration patterns
|
|
25
|
+
|
|
26
|
+
The agent returns structured output with a risk level and brief reasoning.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
"""Agent name for internal use."""
|
|
32
|
+
return "shell_safety_checker"
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def display_name(self) -> str:
|
|
36
|
+
"""User-facing display name."""
|
|
37
|
+
return "Shell Safety Checker 🛡️"
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def description(self) -> str:
|
|
41
|
+
"""Agent description."""
|
|
42
|
+
return "Lightweight agent that assesses shell command safety risks"
|
|
43
|
+
|
|
44
|
+
def get_system_prompt(self) -> str:
|
|
45
|
+
"""Get the ultra-concise system prompt for shell safety assessment.
|
|
46
|
+
|
|
47
|
+
This prompt is kept under 200 tokens for fast inference and low cost.
|
|
48
|
+
"""
|
|
49
|
+
return """You are a shell command safety analyzer. Assess risk levels concisely.
|
|
50
|
+
|
|
51
|
+
**Risk Levels:**
|
|
52
|
+
- none: Completely safe (ls, pwd, echo, cat readonly files)
|
|
53
|
+
- low: Minimal risk (mkdir, touch, git status, read-only queries)
|
|
54
|
+
- medium: Moderate risk (file edits, package installs, service restarts)
|
|
55
|
+
- high: Significant risk (rm files, UPDATE/DELETE without WHERE, TRUNCATE, chmod dangerous permissions)
|
|
56
|
+
- critical: Severe/destructive (rm -rf, DROP TABLE/DATABASE, dd, format, mkfs, bq delete dataset, unfiltered mass deletes)
|
|
57
|
+
|
|
58
|
+
**Evaluate:**
|
|
59
|
+
- Scope (single file vs. entire system)
|
|
60
|
+
- Reversibility (can it be undone?)
|
|
61
|
+
- Data loss potential
|
|
62
|
+
- Privilege requirements
|
|
63
|
+
- Database destruction patterns
|
|
64
|
+
|
|
65
|
+
**Output:** Risk level + reasoning (max 1 sentence)."""
|
|
66
|
+
|
|
67
|
+
def get_available_tools(self) -> List[str]:
|
|
68
|
+
"""This agent uses no tools - pure reasoning only."""
|
|
69
|
+
return []
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Caching layer for shell command safety assessments.
|
|
2
|
+
|
|
3
|
+
This module provides an LRU cache for recently assessed commands to avoid redundant API calls.
|
|
4
|
+
|
|
5
|
+
The approach is simple and secure: let the LLM assess ALL commands and cache
|
|
6
|
+
those assessments. This eliminates the security risks of pre-defined whitelists
|
|
7
|
+
while providing the performance benefits of caching.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections import OrderedDict
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Optional, Tuple
|
|
13
|
+
|
|
14
|
+
# Maximum number of cached assessments (LRU eviction after this)
|
|
15
|
+
MAX_CACHE_SIZE = 200
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CachedAssessment:
|
|
20
|
+
"""A cached safety assessment result."""
|
|
21
|
+
|
|
22
|
+
risk: str
|
|
23
|
+
reasoning: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CommandSafetyCache:
|
|
27
|
+
"""LRU cache for shell command safety assessments.
|
|
28
|
+
|
|
29
|
+
This cache stores previous LLM assessments to avoid redundant API calls.
|
|
30
|
+
It uses an OrderedDict for O(1) LRU eviction.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, max_size: int = MAX_CACHE_SIZE):
|
|
34
|
+
self._cache: OrderedDict[Tuple[str, Optional[str]], CachedAssessment] = (
|
|
35
|
+
OrderedDict()
|
|
36
|
+
)
|
|
37
|
+
self._max_size = max_size
|
|
38
|
+
self._hits = 0
|
|
39
|
+
self._misses = 0
|
|
40
|
+
|
|
41
|
+
def _make_key(self, command: str, cwd: Optional[str]) -> Tuple[str, Optional[str]]:
|
|
42
|
+
"""Create a cache key from command and cwd."""
|
|
43
|
+
# Normalize command (strip whitespace)
|
|
44
|
+
return (command.strip(), cwd)
|
|
45
|
+
|
|
46
|
+
def get(
|
|
47
|
+
self, command: str, cwd: Optional[str] = None
|
|
48
|
+
) -> Optional[CachedAssessment]:
|
|
49
|
+
"""Get a cached assessment if it exists.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
command: The shell command
|
|
53
|
+
cwd: Optional working directory
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
CachedAssessment if found, None otherwise
|
|
57
|
+
"""
|
|
58
|
+
key = self._make_key(command, cwd)
|
|
59
|
+
if key in self._cache:
|
|
60
|
+
# Move to end (most recently used)
|
|
61
|
+
self._cache.move_to_end(key)
|
|
62
|
+
self._hits += 1
|
|
63
|
+
return self._cache[key]
|
|
64
|
+
self._misses += 1
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def put(
|
|
68
|
+
self, command: str, cwd: Optional[str], assessment: CachedAssessment
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Store an assessment in the cache.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
command: The shell command
|
|
74
|
+
cwd: Optional working directory
|
|
75
|
+
assessment: The assessment result to cache
|
|
76
|
+
"""
|
|
77
|
+
key = self._make_key(command, cwd)
|
|
78
|
+
|
|
79
|
+
# If already exists, update and move to end
|
|
80
|
+
if key in self._cache:
|
|
81
|
+
self._cache.move_to_end(key)
|
|
82
|
+
self._cache[key] = assessment
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Evict oldest if at capacity
|
|
86
|
+
while len(self._cache) >= self._max_size:
|
|
87
|
+
self._cache.popitem(last=False)
|
|
88
|
+
|
|
89
|
+
self._cache[key] = assessment
|
|
90
|
+
|
|
91
|
+
def clear(self) -> None:
|
|
92
|
+
"""Clear all cached assessments."""
|
|
93
|
+
self._cache.clear()
|
|
94
|
+
self._hits = 0
|
|
95
|
+
self._misses = 0
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def stats(self) -> dict:
|
|
99
|
+
"""Get cache statistics."""
|
|
100
|
+
total = self._hits + self._misses
|
|
101
|
+
hit_rate = (self._hits / total * 100) if total > 0 else 0
|
|
102
|
+
return {
|
|
103
|
+
"size": len(self._cache),
|
|
104
|
+
"max_size": self._max_size,
|
|
105
|
+
"hits": self._hits,
|
|
106
|
+
"misses": self._misses,
|
|
107
|
+
"hit_rate": f"{hit_rate:.1f}%",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Global cache instance (singleton for the session)
|
|
112
|
+
_cache = CommandSafetyCache()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_cache_stats() -> dict:
|
|
116
|
+
"""Get statistics about the cache performance."""
|
|
117
|
+
return _cache.stats
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_cached_assessment(
|
|
121
|
+
command: str, cwd: Optional[str] = None
|
|
122
|
+
) -> Optional[CachedAssessment]:
|
|
123
|
+
"""Get a cached command safety assessment.
|
|
124
|
+
|
|
125
|
+
Cache-only approach: use the LLM cache for speed, but let the LLM
|
|
126
|
+
determine safety for all commands. No pre-defined whitelists.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
command: The shell command to check
|
|
130
|
+
cwd: Optional working directory
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
CachedAssessment if found in cache, None if needs LLM assessment
|
|
134
|
+
"""
|
|
135
|
+
return _cache.get(command, cwd)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def cache_assessment(
|
|
139
|
+
command: str, cwd: Optional[str], risk: str, reasoning: str
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Cache an LLM assessment result.
|
|
142
|
+
|
|
143
|
+
Cache all LLM assessments since the same command should get
|
|
144
|
+
the same assessment, providing both security and performance.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
command: The shell command
|
|
148
|
+
cwd: Optional working directory
|
|
149
|
+
risk: The assessed risk level
|
|
150
|
+
reasoning: The assessment reasoning
|
|
151
|
+
"""
|
|
152
|
+
assessment = CachedAssessment(
|
|
153
|
+
risk=risk,
|
|
154
|
+
reasoning=reasoning,
|
|
155
|
+
)
|
|
156
|
+
_cache.put(command, cwd, assessment)
|