codepp 0.0.437__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 +117 -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 +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -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 +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -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 +2156 -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 +304 -0
- code_puppy/agents/pack/husky.py +327 -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 +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 +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -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 +532 -0
- code_puppy/command_line/command_handler.py +293 -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 +867 -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 +96 -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/shell_passthrough.py +145 -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 +1770 -0
- code_puppy/error_logging.py +134 -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 +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -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 +1158 -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 +95 -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 +174 -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 +60 -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 +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -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 +640 -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 +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -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 +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -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 +356 -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 +501 -0
- code_puppy/tools/agent_tools.py +603 -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 +26 -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 +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -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
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Scheduler daemon for Code Puppy.
|
|
2
|
+
|
|
3
|
+
Runs as a background process, checking for and executing scheduled tasks.
|
|
4
|
+
Uses pure Python timing (no external scheduler dependencies).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import atexit
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import signal
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from code_puppy.scheduler.config import (
|
|
18
|
+
SCHEDULER_PID_FILE,
|
|
19
|
+
ScheduledTask,
|
|
20
|
+
load_tasks,
|
|
21
|
+
)
|
|
22
|
+
from code_puppy.scheduler.executor import execute_task
|
|
23
|
+
|
|
24
|
+
# Global flag for graceful shutdown
|
|
25
|
+
_shutdown_requested = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_interval(interval_str: str) -> Optional[timedelta]:
|
|
29
|
+
"""Parse interval string like '30m', '1h', '2d' into timedelta."""
|
|
30
|
+
match = re.match(r"^(\d+)([smhd])$", interval_str.lower())
|
|
31
|
+
if not match:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
value = int(match.group(1))
|
|
35
|
+
unit = match.group(2)
|
|
36
|
+
|
|
37
|
+
if unit == "s":
|
|
38
|
+
return timedelta(seconds=value)
|
|
39
|
+
elif unit == "m":
|
|
40
|
+
return timedelta(minutes=value)
|
|
41
|
+
elif unit == "h":
|
|
42
|
+
return timedelta(hours=value)
|
|
43
|
+
elif unit == "d":
|
|
44
|
+
return timedelta(days=value)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def should_run_task(task: ScheduledTask, now: datetime) -> bool:
|
|
49
|
+
"""Determine if a task should run now based on its schedule."""
|
|
50
|
+
if not task.enabled:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
if task.schedule_type == "interval":
|
|
54
|
+
interval = parse_interval(task.schedule_value)
|
|
55
|
+
if not interval:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
if not task.last_run:
|
|
59
|
+
return True # Never run before
|
|
60
|
+
|
|
61
|
+
last_run = datetime.fromisoformat(task.last_run)
|
|
62
|
+
return (now - last_run) >= interval
|
|
63
|
+
|
|
64
|
+
elif task.schedule_type == "hourly":
|
|
65
|
+
if not task.last_run:
|
|
66
|
+
return True
|
|
67
|
+
last_run = datetime.fromisoformat(task.last_run)
|
|
68
|
+
return (now - last_run) >= timedelta(hours=1)
|
|
69
|
+
|
|
70
|
+
elif task.schedule_type == "daily":
|
|
71
|
+
if not task.last_run:
|
|
72
|
+
return True
|
|
73
|
+
last_run = datetime.fromisoformat(task.last_run)
|
|
74
|
+
return (now - last_run) >= timedelta(days=1)
|
|
75
|
+
|
|
76
|
+
elif task.schedule_type == "cron":
|
|
77
|
+
# Cron expressions not yet supported - would need croniter library
|
|
78
|
+
# Log warning so users know why task isn't running
|
|
79
|
+
print(
|
|
80
|
+
f"[Scheduler] Warning: Cron schedules not yet supported, skipping: {task.name}"
|
|
81
|
+
)
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_scheduler_loop(check_interval: int = 60):
|
|
88
|
+
"""Main scheduler loop. Checks tasks every `check_interval` seconds."""
|
|
89
|
+
global _shutdown_requested
|
|
90
|
+
|
|
91
|
+
print(f"[Scheduler] Starting daemon (PID: {os.getpid()})")
|
|
92
|
+
print(f"[Scheduler] Check interval: {check_interval}s")
|
|
93
|
+
|
|
94
|
+
while not _shutdown_requested:
|
|
95
|
+
try:
|
|
96
|
+
tasks = load_tasks()
|
|
97
|
+
now = datetime.now()
|
|
98
|
+
|
|
99
|
+
for task in tasks:
|
|
100
|
+
if _shutdown_requested:
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
if should_run_task(task, now):
|
|
104
|
+
print(f"[Scheduler] Running task: {task.name} ({task.id})")
|
|
105
|
+
success, exit_code, error = execute_task(task)
|
|
106
|
+
if success:
|
|
107
|
+
print(f"[Scheduler] Task completed: {task.name}")
|
|
108
|
+
else:
|
|
109
|
+
print(f"[Scheduler] Task failed: {task.name} - {error}")
|
|
110
|
+
|
|
111
|
+
# Sleep in small increments to allow graceful shutdown
|
|
112
|
+
for _ in range(check_interval):
|
|
113
|
+
if _shutdown_requested:
|
|
114
|
+
break
|
|
115
|
+
time.sleep(1)
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print(f"[Scheduler] Error in loop: {e}")
|
|
119
|
+
time.sleep(10) # Wait before retrying
|
|
120
|
+
|
|
121
|
+
print("[Scheduler] Daemon stopped")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def write_pid_file():
|
|
125
|
+
"""Write the current PID to the PID file atomically.
|
|
126
|
+
|
|
127
|
+
Writes to a temp file first, then atomically replaces the PID file.
|
|
128
|
+
This prevents corruption if a crash occurs mid-write.
|
|
129
|
+
"""
|
|
130
|
+
pid_dir = os.path.dirname(SCHEDULER_PID_FILE)
|
|
131
|
+
os.makedirs(pid_dir, exist_ok=True)
|
|
132
|
+
fd, tmp_path = tempfile.mkstemp(dir=pid_dir, prefix=".pid_tmp_")
|
|
133
|
+
try:
|
|
134
|
+
with os.fdopen(fd, "w") as f:
|
|
135
|
+
f.write(str(os.getpid()))
|
|
136
|
+
os.replace(tmp_path, SCHEDULER_PID_FILE)
|
|
137
|
+
except BaseException:
|
|
138
|
+
try:
|
|
139
|
+
os.remove(tmp_path)
|
|
140
|
+
except OSError:
|
|
141
|
+
pass
|
|
142
|
+
raise
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def remove_pid_file():
|
|
146
|
+
"""Remove the PID file."""
|
|
147
|
+
try:
|
|
148
|
+
if os.path.exists(SCHEDULER_PID_FILE):
|
|
149
|
+
os.remove(SCHEDULER_PID_FILE)
|
|
150
|
+
except OSError:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def signal_handler(signum, frame):
|
|
155
|
+
"""Handle shutdown signals."""
|
|
156
|
+
global _shutdown_requested
|
|
157
|
+
print(f"\n[Scheduler] Received signal {signum}, shutting down...")
|
|
158
|
+
_shutdown_requested = True
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def start_daemon(foreground: bool = False):
|
|
162
|
+
"""Start the scheduler daemon.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
foreground: If True, run in foreground. If False, daemonize.
|
|
166
|
+
"""
|
|
167
|
+
global _shutdown_requested
|
|
168
|
+
_shutdown_requested = False
|
|
169
|
+
|
|
170
|
+
# Set up signal handlers
|
|
171
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
172
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
173
|
+
|
|
174
|
+
# Write PID file and register cleanup
|
|
175
|
+
write_pid_file()
|
|
176
|
+
atexit.register(remove_pid_file)
|
|
177
|
+
|
|
178
|
+
# Run the scheduler loop
|
|
179
|
+
run_scheduler_loop()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_daemon_pid() -> Optional[int]:
|
|
183
|
+
"""Get the PID of the running daemon, or None if not running."""
|
|
184
|
+
if not os.path.exists(SCHEDULER_PID_FILE):
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
with open(SCHEDULER_PID_FILE, "r") as f:
|
|
189
|
+
content = f.read().strip()
|
|
190
|
+
if not content:
|
|
191
|
+
remove_pid_file()
|
|
192
|
+
return None
|
|
193
|
+
pid = int(content)
|
|
194
|
+
|
|
195
|
+
# Check if process is actually running
|
|
196
|
+
if sys.platform == "win32":
|
|
197
|
+
import ctypes
|
|
198
|
+
|
|
199
|
+
kernel32 = ctypes.windll.kernel32
|
|
200
|
+
handle = kernel32.OpenProcess(
|
|
201
|
+
0x1000, False, pid
|
|
202
|
+
) # PROCESS_QUERY_LIMITED_INFORMATION
|
|
203
|
+
if handle:
|
|
204
|
+
kernel32.CloseHandle(handle)
|
|
205
|
+
return pid
|
|
206
|
+
return None
|
|
207
|
+
else:
|
|
208
|
+
os.kill(pid, 0) # Doesn't kill, just checks if process exists
|
|
209
|
+
return pid
|
|
210
|
+
except (ValueError, ProcessLookupError, PermissionError, OSError):
|
|
211
|
+
# PID file exists but process is not running - stale PID file
|
|
212
|
+
remove_pid_file()
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def start_daemon_background() -> bool:
|
|
217
|
+
"""Start the scheduler daemon in the background.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
True if daemon started successfully, False otherwise.
|
|
221
|
+
"""
|
|
222
|
+
import subprocess
|
|
223
|
+
import time
|
|
224
|
+
|
|
225
|
+
# NOTE: There is an inherent TOCTOU race between checking if the daemon
|
|
226
|
+
# is running and starting a new one. Full mitigation would require file
|
|
227
|
+
# locking (e.g., fcntl.flock). We mitigate by re-checking after start.
|
|
228
|
+
pid = get_daemon_pid()
|
|
229
|
+
if pid:
|
|
230
|
+
return True # Already running
|
|
231
|
+
|
|
232
|
+
cmd = [sys.executable, "-m", "code_puppy.scheduler"]
|
|
233
|
+
|
|
234
|
+
if sys.platform == "win32":
|
|
235
|
+
subprocess.Popen(
|
|
236
|
+
cmd,
|
|
237
|
+
creationflags=subprocess.CREATE_NO_WINDOW,
|
|
238
|
+
stdout=subprocess.DEVNULL,
|
|
239
|
+
stderr=subprocess.DEVNULL,
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
subprocess.Popen(
|
|
243
|
+
cmd,
|
|
244
|
+
start_new_session=True,
|
|
245
|
+
stdout=subprocess.DEVNULL,
|
|
246
|
+
stderr=subprocess.DEVNULL,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
time.sleep(1)
|
|
250
|
+
# Re-check to confirm daemon actually started (also catches race where
|
|
251
|
+
# another process started the daemon between our check and Popen).
|
|
252
|
+
return get_daemon_pid() is not None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def stop_daemon() -> bool:
|
|
256
|
+
"""Stop the running daemon. Returns True if stopped successfully."""
|
|
257
|
+
pid = get_daemon_pid()
|
|
258
|
+
if not pid:
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
if sys.platform == "win32":
|
|
263
|
+
import ctypes
|
|
264
|
+
|
|
265
|
+
kernel32 = ctypes.windll.kernel32
|
|
266
|
+
handle = kernel32.OpenProcess(1, False, pid) # PROCESS_TERMINATE
|
|
267
|
+
kernel32.TerminateProcess(handle, 0)
|
|
268
|
+
kernel32.CloseHandle(handle)
|
|
269
|
+
else:
|
|
270
|
+
os.kill(pid, signal.SIGTERM)
|
|
271
|
+
|
|
272
|
+
# Wait for process to stop
|
|
273
|
+
for _ in range(10):
|
|
274
|
+
time.sleep(0.5)
|
|
275
|
+
if not get_daemon_pid():
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
return False
|
|
279
|
+
except Exception:
|
|
280
|
+
return False
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Task executor for the Code Puppy 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
|