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,155 @@
|
|
|
1
|
+
"""Task executor for the scheduler.
|
|
2
|
+
|
|
3
|
+
Handles executing scheduled tasks by invoking code-puppy CLI
|
|
4
|
+
with the configured prompt, model, and agent.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
|
|
13
|
+
from code_puppy.scheduler.config import (
|
|
14
|
+
SCHEDULER_LOG_DIR,
|
|
15
|
+
ScheduledTask,
|
|
16
|
+
update_task,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_code_puppy_command() -> str:
|
|
21
|
+
"""Get the path to the code-puppy executable."""
|
|
22
|
+
# Try to find code-puppy in the same environment as this script
|
|
23
|
+
if sys.platform == "win32":
|
|
24
|
+
# On Windows, look for code-puppy.exe or use python -m
|
|
25
|
+
return "code-puppy"
|
|
26
|
+
else:
|
|
27
|
+
# On Unix, code-puppy should be in PATH if installed
|
|
28
|
+
return "code-puppy"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def execute_task(task: ScheduledTask) -> Tuple[bool, int, str]:
|
|
32
|
+
"""Execute a scheduled task.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
task: The ScheduledTask to execute
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tuple of (success: bool, exit_code: int, error_message: str)
|
|
39
|
+
"""
|
|
40
|
+
# Ensure log directory exists
|
|
41
|
+
os.makedirs(SCHEDULER_LOG_DIR, mode=0o700, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
# Build the command
|
|
44
|
+
cmd = [get_code_puppy_command()]
|
|
45
|
+
|
|
46
|
+
# Add prompt
|
|
47
|
+
cmd.extend(["-p", task.prompt])
|
|
48
|
+
|
|
49
|
+
# Add model if specified
|
|
50
|
+
if task.model:
|
|
51
|
+
cmd.extend(["--model", task.model])
|
|
52
|
+
|
|
53
|
+
# Add agent if specified
|
|
54
|
+
if task.agent:
|
|
55
|
+
cmd.extend(["--agent", task.agent])
|
|
56
|
+
|
|
57
|
+
# Determine working directory
|
|
58
|
+
working_dir = task.working_directory
|
|
59
|
+
if working_dir == "." or not working_dir:
|
|
60
|
+
working_dir = os.getcwd()
|
|
61
|
+
working_dir = os.path.expanduser(working_dir)
|
|
62
|
+
|
|
63
|
+
# Validate working directory exists
|
|
64
|
+
if not os.path.isdir(working_dir):
|
|
65
|
+
error_msg = f"Working directory not found: {working_dir}"
|
|
66
|
+
task.last_status = "failed"
|
|
67
|
+
task.last_exit_code = -1
|
|
68
|
+
update_task(task)
|
|
69
|
+
return (False, -1, error_msg)
|
|
70
|
+
|
|
71
|
+
# Ensure log file path
|
|
72
|
+
log_file = task.log_file
|
|
73
|
+
if not log_file:
|
|
74
|
+
log_file = os.path.join(SCHEDULER_LOG_DIR, f"{task.id}.log")
|
|
75
|
+
log_file = os.path.expanduser(log_file)
|
|
76
|
+
|
|
77
|
+
# Ensure log file directory exists
|
|
78
|
+
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
|
79
|
+
|
|
80
|
+
# Update task status to running
|
|
81
|
+
task.last_status = "running"
|
|
82
|
+
task.last_run = datetime.now().isoformat()
|
|
83
|
+
update_task(task)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Open log file for appending
|
|
87
|
+
with open(log_file, "a") as log_f:
|
|
88
|
+
# Write header
|
|
89
|
+
log_f.write(f"\n{'=' * 60}\n")
|
|
90
|
+
log_f.write(f"Task: {task.name} ({task.id})\n")
|
|
91
|
+
log_f.write(f"Started: {datetime.now().isoformat()}\n")
|
|
92
|
+
log_f.write(f"Command: {' '.join(cmd)}\n")
|
|
93
|
+
log_f.write(f"Working Dir: {working_dir}\n")
|
|
94
|
+
log_f.write(f"{'=' * 60}\n\n")
|
|
95
|
+
log_f.flush()
|
|
96
|
+
|
|
97
|
+
# Execute the command
|
|
98
|
+
process = subprocess.Popen(
|
|
99
|
+
cmd,
|
|
100
|
+
cwd=working_dir,
|
|
101
|
+
stdout=log_f,
|
|
102
|
+
stderr=subprocess.STDOUT,
|
|
103
|
+
shell=False,
|
|
104
|
+
env=os.environ.copy(),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Wait for completion
|
|
108
|
+
exit_code = process.wait()
|
|
109
|
+
|
|
110
|
+
# Write footer
|
|
111
|
+
log_f.write(f"\n{'=' * 60}\n")
|
|
112
|
+
log_f.write(f"Finished: {datetime.now().isoformat()}\n")
|
|
113
|
+
log_f.write(f"Exit Code: {exit_code}\n")
|
|
114
|
+
log_f.write(f"{'=' * 60}\n")
|
|
115
|
+
|
|
116
|
+
# Update task status
|
|
117
|
+
task.last_status = "success" if exit_code == 0 else "failed"
|
|
118
|
+
task.last_exit_code = exit_code
|
|
119
|
+
update_task(task)
|
|
120
|
+
|
|
121
|
+
return (exit_code == 0, exit_code, "")
|
|
122
|
+
|
|
123
|
+
except FileNotFoundError as e:
|
|
124
|
+
error_msg = f"code-puppy not found: {e}"
|
|
125
|
+
task.last_status = "failed"
|
|
126
|
+
task.last_exit_code = -1
|
|
127
|
+
update_task(task)
|
|
128
|
+
return (False, -1, error_msg)
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
error_msg = f"Execution error: {e}"
|
|
132
|
+
task.last_status = "failed"
|
|
133
|
+
task.last_exit_code = -1
|
|
134
|
+
update_task(task)
|
|
135
|
+
return (False, -1, error_msg)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_task_by_id(task_id: str) -> Tuple[bool, str]:
|
|
139
|
+
"""Run a task immediately by its ID.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Tuple of (success: bool, message: str)
|
|
143
|
+
"""
|
|
144
|
+
from code_puppy.scheduler.config import get_task
|
|
145
|
+
|
|
146
|
+
task = get_task(task_id)
|
|
147
|
+
if not task:
|
|
148
|
+
return (False, f"Task not found: {task_id}")
|
|
149
|
+
|
|
150
|
+
success, exit_code, error = execute_task(task)
|
|
151
|
+
|
|
152
|
+
if success:
|
|
153
|
+
return (True, f"Task '{task.name}' completed successfully")
|
|
154
|
+
else:
|
|
155
|
+
return (False, f"Task '{task.name}' failed (exit code: {exit_code}): {error}")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Platform abstraction for daemon management.
|
|
2
|
+
|
|
3
|
+
Provides a unified interface for daemon operations across Windows, Linux, and macOS.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
if sys.platform == "win32":
|
|
9
|
+
from code_puppy.scheduler.platform_win import (
|
|
10
|
+
is_process_running,
|
|
11
|
+
terminate_process,
|
|
12
|
+
)
|
|
13
|
+
else:
|
|
14
|
+
from code_puppy.scheduler.platform_unix import (
|
|
15
|
+
is_process_running,
|
|
16
|
+
terminate_process,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = ["is_process_running", "terminate_process"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Unix/macOS platform support for scheduler daemon."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_process_running(pid: int) -> bool:
|
|
8
|
+
"""Check if a process with the given PID is running."""
|
|
9
|
+
try:
|
|
10
|
+
os.kill(pid, 0)
|
|
11
|
+
return True
|
|
12
|
+
except (ProcessLookupError, PermissionError):
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def terminate_process(pid: int) -> bool:
|
|
17
|
+
"""Terminate a process by PID."""
|
|
18
|
+
try:
|
|
19
|
+
os.kill(pid, signal.SIGTERM)
|
|
20
|
+
return True
|
|
21
|
+
except (ProcessLookupError, PermissionError):
|
|
22
|
+
return False
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Windows platform support for scheduler daemon."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_process_running(pid: int) -> bool:
|
|
7
|
+
"""Check if a process with the given PID is running."""
|
|
8
|
+
try:
|
|
9
|
+
kernel32 = ctypes.windll.kernel32
|
|
10
|
+
handle = kernel32.OpenProcess(
|
|
11
|
+
0x1000, False, pid
|
|
12
|
+
) # PROCESS_QUERY_LIMITED_INFORMATION
|
|
13
|
+
if handle:
|
|
14
|
+
kernel32.CloseHandle(handle)
|
|
15
|
+
return True
|
|
16
|
+
return False
|
|
17
|
+
except Exception:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def terminate_process(pid: int) -> bool:
|
|
22
|
+
"""Terminate a process by PID."""
|
|
23
|
+
try:
|
|
24
|
+
kernel32 = ctypes.windll.kernel32
|
|
25
|
+
handle = kernel32.OpenProcess(1, False, pid) # PROCESS_TERMINATE
|
|
26
|
+
if handle:
|
|
27
|
+
kernel32.TerminateProcess(handle, 0)
|
|
28
|
+
kernel32.CloseHandle(handle)
|
|
29
|
+
return True
|
|
30
|
+
return False
|
|
31
|
+
except Exception:
|
|
32
|
+
return False
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Shared helpers for persisting and restoring chat sessions.
|
|
2
|
+
|
|
3
|
+
This module centralises the pickle + metadata handling that used to live in
|
|
4
|
+
both the CLI command handler and the auto-save feature. Keeping it here helps
|
|
5
|
+
us avoid duplication while staying inside the Zen-of-Python sweet spot: simple
|
|
6
|
+
is better than complex, nested side effects are worse than deliberate helpers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import pickle
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Callable, List
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _safe_loads(data: bytes) -> Any:
|
|
19
|
+
"""Deserialize pickle data."""
|
|
20
|
+
return pickle.loads(data) # noqa: S301
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_LEGACY_SIGNED_HEADER = b"CPSESSION\x01"
|
|
24
|
+
_LEGACY_SIGNATURE_SIZE = (
|
|
25
|
+
32 # legacy signature bytes, retained only for backward-compat parsing
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
SessionHistory = List[Any]
|
|
29
|
+
TokenEstimator = Callable[[Any], int]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class SessionPaths:
|
|
34
|
+
pickle_path: Path
|
|
35
|
+
metadata_path: Path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class SessionMetadata:
|
|
40
|
+
session_name: str
|
|
41
|
+
timestamp: str
|
|
42
|
+
message_count: int
|
|
43
|
+
total_tokens: int
|
|
44
|
+
pickle_path: Path
|
|
45
|
+
metadata_path: Path
|
|
46
|
+
auto_saved: bool = False
|
|
47
|
+
|
|
48
|
+
def as_serialisable(self) -> dict[str, Any]:
|
|
49
|
+
return {
|
|
50
|
+
"session_name": self.session_name,
|
|
51
|
+
"timestamp": self.timestamp,
|
|
52
|
+
"message_count": self.message_count,
|
|
53
|
+
"total_tokens": self.total_tokens,
|
|
54
|
+
"file_path": str(self.pickle_path),
|
|
55
|
+
"auto_saved": self.auto_saved,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _extract_pickle_payload(raw: bytes) -> bytes:
|
|
60
|
+
"""Return the pickle payload from raw session file bytes.
|
|
61
|
+
|
|
62
|
+
New format is raw pickle bytes.
|
|
63
|
+
Legacy format was: header + 32-byte signature + pickle payload.
|
|
64
|
+
We no longer verify or generate signatures.
|
|
65
|
+
"""
|
|
66
|
+
if raw.startswith(_LEGACY_SIGNED_HEADER):
|
|
67
|
+
offset = len(_LEGACY_SIGNED_HEADER) + _LEGACY_SIGNATURE_SIZE
|
|
68
|
+
return raw[offset:]
|
|
69
|
+
return raw
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ensure_directory(path: Path) -> Path:
|
|
73
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
return path
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_session_paths(base_dir: Path, session_name: str) -> SessionPaths:
|
|
78
|
+
pickle_path = base_dir / f"{session_name}.pkl"
|
|
79
|
+
metadata_path = base_dir / f"{session_name}_meta.json"
|
|
80
|
+
return SessionPaths(pickle_path=pickle_path, metadata_path=metadata_path)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def save_session(
|
|
84
|
+
*,
|
|
85
|
+
history: SessionHistory,
|
|
86
|
+
session_name: str,
|
|
87
|
+
base_dir: Path,
|
|
88
|
+
timestamp: str,
|
|
89
|
+
token_estimator: TokenEstimator,
|
|
90
|
+
auto_saved: bool = False,
|
|
91
|
+
) -> SessionMetadata:
|
|
92
|
+
ensure_directory(base_dir)
|
|
93
|
+
paths = build_session_paths(base_dir, session_name)
|
|
94
|
+
|
|
95
|
+
pickle_data = pickle.dumps(history)
|
|
96
|
+
tmp_pickle = paths.pickle_path.with_suffix(".tmp")
|
|
97
|
+
with tmp_pickle.open("wb") as pickle_file:
|
|
98
|
+
pickle_file.write(pickle_data)
|
|
99
|
+
tmp_pickle.replace(paths.pickle_path)
|
|
100
|
+
|
|
101
|
+
total_tokens = sum(token_estimator(message) for message in history)
|
|
102
|
+
metadata = SessionMetadata(
|
|
103
|
+
session_name=session_name,
|
|
104
|
+
timestamp=timestamp,
|
|
105
|
+
message_count=len(history),
|
|
106
|
+
total_tokens=total_tokens,
|
|
107
|
+
pickle_path=paths.pickle_path,
|
|
108
|
+
metadata_path=paths.metadata_path,
|
|
109
|
+
auto_saved=auto_saved,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
tmp_metadata = paths.metadata_path.with_suffix(".tmp")
|
|
113
|
+
with tmp_metadata.open("w", encoding="utf-8") as metadata_file:
|
|
114
|
+
json.dump(metadata.as_serialisable(), metadata_file, indent=2)
|
|
115
|
+
tmp_metadata.replace(paths.metadata_path)
|
|
116
|
+
|
|
117
|
+
return metadata
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_session(
|
|
121
|
+
session_name: str, base_dir: Path, *, allow_legacy: bool = False
|
|
122
|
+
) -> SessionHistory:
|
|
123
|
+
# Kept for API compatibility; legacy loading is always supported now.
|
|
124
|
+
_ = allow_legacy
|
|
125
|
+
|
|
126
|
+
paths = build_session_paths(base_dir, session_name)
|
|
127
|
+
if not paths.pickle_path.exists():
|
|
128
|
+
raise FileNotFoundError(paths.pickle_path)
|
|
129
|
+
|
|
130
|
+
raw = paths.pickle_path.read_bytes()
|
|
131
|
+
pickle_data = _extract_pickle_payload(raw)
|
|
132
|
+
return _safe_loads(pickle_data)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def list_sessions(base_dir: Path) -> List[str]:
|
|
136
|
+
if not base_dir.exists():
|
|
137
|
+
return []
|
|
138
|
+
return sorted(path.stem for path in base_dir.glob("*.pkl"))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cleanup_sessions(base_dir: Path, max_sessions: int) -> List[str]:
|
|
142
|
+
if max_sessions <= 0:
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
if not base_dir.exists():
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
candidate_paths = list(base_dir.glob("*.pkl"))
|
|
149
|
+
if len(candidate_paths) <= max_sessions:
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
sorted_candidates = sorted(
|
|
153
|
+
((path.stat().st_mtime, path) for path in candidate_paths),
|
|
154
|
+
key=lambda item: item[0],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
stale_entries = sorted_candidates[:-max_sessions]
|
|
158
|
+
removed_sessions: List[str] = []
|
|
159
|
+
for _, pickle_path in stale_entries:
|
|
160
|
+
metadata_path = base_dir / f"{pickle_path.stem}_meta.json"
|
|
161
|
+
try:
|
|
162
|
+
pickle_path.unlink(missing_ok=True)
|
|
163
|
+
metadata_path.unlink(missing_ok=True)
|
|
164
|
+
removed_sessions.append(pickle_path.stem)
|
|
165
|
+
except OSError:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
return removed_sessions
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def restore_autosave_interactively(base_dir: Path) -> None:
|
|
172
|
+
"""Prompt the user to load an autosave session from base_dir, if any exist.
|
|
173
|
+
|
|
174
|
+
This helper is deliberately placed in session_storage to keep autosave
|
|
175
|
+
restoration close to the persistence layer. It uses the same public APIs
|
|
176
|
+
(list_sessions, load_session) and mirrors the interactive behaviours from
|
|
177
|
+
the command handler.
|
|
178
|
+
"""
|
|
179
|
+
sessions = list_sessions(base_dir)
|
|
180
|
+
if not sessions:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# Import locally to avoid pulling the messaging layer into storage modules
|
|
184
|
+
from datetime import datetime
|
|
185
|
+
|
|
186
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
187
|
+
|
|
188
|
+
from code_puppy.agents.agent_manager import get_current_agent
|
|
189
|
+
from code_puppy.command_line.prompt_toolkit_completion import (
|
|
190
|
+
get_input_with_combined_completion,
|
|
191
|
+
)
|
|
192
|
+
from code_puppy.messaging import emit_success, emit_system_message, emit_warning
|
|
193
|
+
|
|
194
|
+
entries = []
|
|
195
|
+
for name in sessions:
|
|
196
|
+
meta_path = base_dir / f"{name}_meta.json"
|
|
197
|
+
try:
|
|
198
|
+
with meta_path.open("r", encoding="utf-8") as meta_file:
|
|
199
|
+
data = json.load(meta_file)
|
|
200
|
+
timestamp = data.get("timestamp")
|
|
201
|
+
message_count = data.get("message_count")
|
|
202
|
+
except Exception:
|
|
203
|
+
timestamp = None
|
|
204
|
+
message_count = None
|
|
205
|
+
entries.append((name, timestamp, message_count))
|
|
206
|
+
|
|
207
|
+
def sort_key(entry):
|
|
208
|
+
_, timestamp, _ = entry
|
|
209
|
+
if timestamp:
|
|
210
|
+
try:
|
|
211
|
+
return datetime.fromisoformat(timestamp)
|
|
212
|
+
except ValueError:
|
|
213
|
+
return datetime.min
|
|
214
|
+
return datetime.min
|
|
215
|
+
|
|
216
|
+
entries.sort(key=sort_key, reverse=True)
|
|
217
|
+
|
|
218
|
+
PAGE_SIZE = 5
|
|
219
|
+
total = len(entries)
|
|
220
|
+
page = 0
|
|
221
|
+
|
|
222
|
+
def render_page() -> None:
|
|
223
|
+
start = page * PAGE_SIZE
|
|
224
|
+
end = min(start + PAGE_SIZE, total)
|
|
225
|
+
page_entries = entries[start:end]
|
|
226
|
+
emit_system_message("Autosave Sessions Available:")
|
|
227
|
+
for idx, (name, timestamp, message_count) in enumerate(page_entries, start=1):
|
|
228
|
+
timestamp_display = timestamp or "unknown time"
|
|
229
|
+
message_display = (
|
|
230
|
+
f"{message_count} messages"
|
|
231
|
+
if message_count is not None
|
|
232
|
+
else "unknown size"
|
|
233
|
+
)
|
|
234
|
+
emit_system_message(
|
|
235
|
+
f" [{idx}] {name} ({message_display}, saved at {timestamp_display})"
|
|
236
|
+
)
|
|
237
|
+
# If there are more pages, offer next-page; show 'Return to first page' on last page
|
|
238
|
+
if total > PAGE_SIZE:
|
|
239
|
+
page_count = (total + PAGE_SIZE - 1) // PAGE_SIZE
|
|
240
|
+
is_last_page = (page + 1) >= page_count
|
|
241
|
+
remaining = total - (page * PAGE_SIZE + len(page_entries))
|
|
242
|
+
summary = (
|
|
243
|
+
f" and {remaining} more" if (remaining > 0 and not is_last_page) else ""
|
|
244
|
+
)
|
|
245
|
+
label = "Return to first page" if is_last_page else f"Next page{summary}"
|
|
246
|
+
emit_system_message(f" [6] {label}")
|
|
247
|
+
emit_system_message(" [Enter] Skip loading autosave")
|
|
248
|
+
|
|
249
|
+
chosen_name: str | None = None
|
|
250
|
+
|
|
251
|
+
while True:
|
|
252
|
+
render_page()
|
|
253
|
+
try:
|
|
254
|
+
selection = await get_input_with_combined_completion(
|
|
255
|
+
FormattedText(
|
|
256
|
+
[
|
|
257
|
+
(
|
|
258
|
+
"class:prompt",
|
|
259
|
+
"Pick 1-5 to load, 6 for next, or name/Enter: ",
|
|
260
|
+
)
|
|
261
|
+
]
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
except (KeyboardInterrupt, EOFError):
|
|
265
|
+
emit_warning("Autosave selection cancelled")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
selection = (selection or "").strip()
|
|
269
|
+
if not selection:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
# Numeric choice: 1-5 select within current page; 6 advances page
|
|
273
|
+
if selection.isdigit():
|
|
274
|
+
num = int(selection)
|
|
275
|
+
if num == 6 and total > PAGE_SIZE:
|
|
276
|
+
page = (page + 1) % ((total + PAGE_SIZE - 1) // PAGE_SIZE)
|
|
277
|
+
# loop and re-render next page
|
|
278
|
+
continue
|
|
279
|
+
if 1 <= num <= 5:
|
|
280
|
+
start = page * PAGE_SIZE
|
|
281
|
+
idx = start + (num - 1)
|
|
282
|
+
if 0 <= idx < total:
|
|
283
|
+
chosen_name = entries[idx][0]
|
|
284
|
+
break
|
|
285
|
+
else:
|
|
286
|
+
emit_warning("Invalid selection for this page")
|
|
287
|
+
continue
|
|
288
|
+
emit_warning("Invalid selection; choose 1-5 or 6 for next")
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
# Allow direct typing by exact session name
|
|
292
|
+
for name, _ts, _mc in entries:
|
|
293
|
+
if name == selection:
|
|
294
|
+
chosen_name = name
|
|
295
|
+
break
|
|
296
|
+
if chosen_name:
|
|
297
|
+
break
|
|
298
|
+
emit_warning("No autosave loaded (invalid selection)")
|
|
299
|
+
# keep looping and allow another try
|
|
300
|
+
|
|
301
|
+
if not chosen_name:
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
history = load_session(chosen_name, base_dir)
|
|
306
|
+
except FileNotFoundError:
|
|
307
|
+
emit_warning(f"Autosave '{chosen_name}' could not be found")
|
|
308
|
+
return
|
|
309
|
+
except Exception as exc:
|
|
310
|
+
emit_warning(f"Failed to load autosave '{chosen_name}': {exc}")
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
agent = get_current_agent()
|
|
314
|
+
agent.set_message_history(history)
|
|
315
|
+
|
|
316
|
+
# Set current autosave session id so subsequent autosaves overwrite this session
|
|
317
|
+
try:
|
|
318
|
+
from code_puppy.config import set_current_autosave_from_session_name
|
|
319
|
+
|
|
320
|
+
set_current_autosave_from_session_name(chosen_name)
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
total_tokens = sum(agent.estimate_tokens_for_message(msg) for msg in history)
|
|
325
|
+
|
|
326
|
+
session_path = base_dir / f"{chosen_name}.pkl"
|
|
327
|
+
emit_success(
|
|
328
|
+
f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
|
329
|
+
f"📁 From: {session_path}"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Display recent message history for context
|
|
333
|
+
try:
|
|
334
|
+
from code_puppy.command_line.autosave_menu import display_resumed_history
|
|
335
|
+
|
|
336
|
+
display_resumed_history(history)
|
|
337
|
+
except Exception:
|
|
338
|
+
pass # Don't fail if display doesn't work in non-TTY environment
|