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,41 @@
|
|
|
1
|
+
"""Scheduler - Run scheduled prompts automatically.
|
|
2
|
+
|
|
3
|
+
This module provides a cross-platform scheduler daemon that executes
|
|
4
|
+
prompts on configurable schedules (intervals, cron expressions).
|
|
5
|
+
|
|
6
|
+
Components:
|
|
7
|
+
- config: Task definitions and JSON persistence
|
|
8
|
+
- daemon: Background scheduler process
|
|
9
|
+
- executor: Task execution logic
|
|
10
|
+
- platform: Cross-platform daemon management
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from code_puppy.scheduler.config import (
|
|
14
|
+
SCHEDULER_LOG_DIR,
|
|
15
|
+
SCHEDULER_PID_FILE,
|
|
16
|
+
SCHEDULES_FILE,
|
|
17
|
+
ScheduledTask,
|
|
18
|
+
add_task,
|
|
19
|
+
delete_task,
|
|
20
|
+
get_task,
|
|
21
|
+
load_tasks,
|
|
22
|
+
save_tasks,
|
|
23
|
+
toggle_task,
|
|
24
|
+
update_task,
|
|
25
|
+
)
|
|
26
|
+
from code_puppy.scheduler.daemon import start_daemon_background
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ScheduledTask",
|
|
30
|
+
"load_tasks",
|
|
31
|
+
"save_tasks",
|
|
32
|
+
"add_task",
|
|
33
|
+
"update_task",
|
|
34
|
+
"delete_task",
|
|
35
|
+
"get_task",
|
|
36
|
+
"toggle_task",
|
|
37
|
+
"start_daemon_background",
|
|
38
|
+
"SCHEDULES_FILE",
|
|
39
|
+
"SCHEDULER_PID_FILE",
|
|
40
|
+
"SCHEDULER_LOG_DIR",
|
|
41
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""CLI subcommands for the scheduler.
|
|
2
|
+
|
|
3
|
+
Handles command-line operations like starting/stopping the daemon,
|
|
4
|
+
listing tasks, and running tasks immediately.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handle_scheduler_start() -> bool:
|
|
11
|
+
"""Start the scheduler daemon in background."""
|
|
12
|
+
from code_puppy.scheduler.daemon import get_daemon_pid, start_daemon_background
|
|
13
|
+
|
|
14
|
+
pid = get_daemon_pid()
|
|
15
|
+
if pid:
|
|
16
|
+
emit_warning(f"Scheduler daemon already running (PID {pid})")
|
|
17
|
+
return True
|
|
18
|
+
|
|
19
|
+
emit_info("Starting scheduler daemon...")
|
|
20
|
+
|
|
21
|
+
if start_daemon_background():
|
|
22
|
+
pid = get_daemon_pid()
|
|
23
|
+
emit_success(f"Scheduler daemon started (PID {pid})")
|
|
24
|
+
return True
|
|
25
|
+
else:
|
|
26
|
+
emit_error("Failed to start scheduler daemon")
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def handle_scheduler_stop() -> bool:
|
|
31
|
+
"""Stop the scheduler daemon."""
|
|
32
|
+
from code_puppy.scheduler.daemon import get_daemon_pid, stop_daemon
|
|
33
|
+
|
|
34
|
+
pid = get_daemon_pid()
|
|
35
|
+
if not pid:
|
|
36
|
+
emit_info("Scheduler daemon is not running")
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
emit_info(f"Stopping scheduler daemon (PID {pid})...")
|
|
40
|
+
|
|
41
|
+
if stop_daemon():
|
|
42
|
+
emit_success("Scheduler daemon stopped")
|
|
43
|
+
return True
|
|
44
|
+
else:
|
|
45
|
+
emit_error("Failed to stop scheduler daemon")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def handle_scheduler_status() -> bool:
|
|
50
|
+
"""Show scheduler daemon status."""
|
|
51
|
+
from code_puppy.scheduler.config import load_tasks
|
|
52
|
+
from code_puppy.scheduler.daemon import get_daemon_pid
|
|
53
|
+
|
|
54
|
+
pid = get_daemon_pid()
|
|
55
|
+
if pid:
|
|
56
|
+
emit_success(f"Scheduler daemon: RUNNING (PID {pid})")
|
|
57
|
+
else:
|
|
58
|
+
emit_warning("Scheduler daemon: STOPPED")
|
|
59
|
+
|
|
60
|
+
tasks = load_tasks()
|
|
61
|
+
enabled_count = sum(1 for t in tasks if t.enabled)
|
|
62
|
+
|
|
63
|
+
emit_info(f"\n📅 Scheduled tasks: {len(tasks)} total, {enabled_count} enabled")
|
|
64
|
+
|
|
65
|
+
if tasks:
|
|
66
|
+
emit_info("\nTasks:")
|
|
67
|
+
for task in tasks:
|
|
68
|
+
status_icon = "🟢" if task.enabled else "🔴"
|
|
69
|
+
last_run = task.last_run[:19] if task.last_run else "never"
|
|
70
|
+
emit_info(
|
|
71
|
+
f" {status_icon} {task.name} ({task.schedule_type}: {task.schedule_value})"
|
|
72
|
+
)
|
|
73
|
+
emit_info(
|
|
74
|
+
f" Last run: {last_run}, Status: {task.last_status or 'pending'}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def handle_scheduler_list() -> bool:
|
|
81
|
+
"""List all scheduled tasks."""
|
|
82
|
+
from code_puppy.scheduler.config import load_tasks
|
|
83
|
+
|
|
84
|
+
tasks = load_tasks()
|
|
85
|
+
|
|
86
|
+
if not tasks:
|
|
87
|
+
emit_info("No scheduled tasks configured.")
|
|
88
|
+
emit_info("Use '/scheduler' to create one.")
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
emit_info(f"📅 Scheduled Tasks ({len(tasks)}):\n")
|
|
92
|
+
|
|
93
|
+
for task in tasks:
|
|
94
|
+
status = "🟢 enabled" if task.enabled else "🔴 disabled"
|
|
95
|
+
emit_info(f" [{task.id}] {task.name}")
|
|
96
|
+
emit_info(f" Status: {status}")
|
|
97
|
+
emit_info(f" Schedule: {task.schedule_type} ({task.schedule_value})")
|
|
98
|
+
emit_info(f" Agent: {task.agent}, Model: {task.model or 'default'}")
|
|
99
|
+
if task.last_run:
|
|
100
|
+
emit_info(f" Last run: {task.last_run[:19]} ({task.last_status})")
|
|
101
|
+
emit_info("")
|
|
102
|
+
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def handle_scheduler_run(task_id: str) -> bool:
|
|
107
|
+
"""Run a specific task immediately."""
|
|
108
|
+
from code_puppy.scheduler.executor import run_task_by_id
|
|
109
|
+
|
|
110
|
+
emit_info(f"Running task {task_id}...")
|
|
111
|
+
success, message = run_task_by_id(task_id)
|
|
112
|
+
|
|
113
|
+
if success:
|
|
114
|
+
emit_success(message)
|
|
115
|
+
else:
|
|
116
|
+
emit_error(message)
|
|
117
|
+
|
|
118
|
+
return success
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Scheduler configuration and task management.
|
|
2
|
+
|
|
3
|
+
Handles ScheduledTask dataclass definition and JSON persistence
|
|
4
|
+
for scheduled tasks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
# Import from existing config
|
|
16
|
+
from code_puppy.config import DATA_DIR
|
|
17
|
+
|
|
18
|
+
SCHEDULES_FILE = os.path.join(DATA_DIR, "scheduled_tasks.json")
|
|
19
|
+
SCHEDULER_PID_FILE = os.path.join(DATA_DIR, "scheduler.pid")
|
|
20
|
+
SCHEDULER_LOG_DIR = os.path.join(DATA_DIR, "scheduler_logs")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ScheduledTask:
|
|
25
|
+
"""A scheduled task."""
|
|
26
|
+
|
|
27
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
|
28
|
+
name: str = ""
|
|
29
|
+
prompt: str = ""
|
|
30
|
+
agent: str = "code-puppy"
|
|
31
|
+
model: str = "" # Uses default if empty
|
|
32
|
+
schedule_type: str = "interval" # "interval", "cron", "daily", "hourly"
|
|
33
|
+
schedule_value: str = "1h" # e.g., "30m", "1h", "0 9 * * *" for cron
|
|
34
|
+
working_directory: str = "."
|
|
35
|
+
log_file: str = "" # Auto-generated if empty
|
|
36
|
+
enabled: bool = True
|
|
37
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
38
|
+
last_run: Optional[str] = None
|
|
39
|
+
last_status: Optional[str] = None # "success", "failed", "running"
|
|
40
|
+
last_exit_code: Optional[int] = None
|
|
41
|
+
|
|
42
|
+
def __post_init__(self):
|
|
43
|
+
if not self.log_file:
|
|
44
|
+
self.log_file = os.path.join(SCHEDULER_LOG_DIR, f"{self.id}.log")
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
return asdict(self)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_dict(cls, data: dict) -> "ScheduledTask":
|
|
51
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def ensure_scheduler_dirs() -> None:
|
|
55
|
+
"""Create scheduler directories if they don't exist."""
|
|
56
|
+
os.makedirs(SCHEDULER_LOG_DIR, mode=0o700, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_tasks() -> List[ScheduledTask]:
|
|
60
|
+
"""Load all scheduled tasks from JSON file."""
|
|
61
|
+
ensure_scheduler_dirs()
|
|
62
|
+
if not os.path.exists(SCHEDULES_FILE):
|
|
63
|
+
return []
|
|
64
|
+
try:
|
|
65
|
+
with open(SCHEDULES_FILE, "r") as f:
|
|
66
|
+
data = json.load(f)
|
|
67
|
+
return [ScheduledTask.from_dict(t) for t in data]
|
|
68
|
+
except (json.JSONDecodeError, IOError):
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def save_tasks(tasks: List[ScheduledTask]) -> None:
|
|
73
|
+
"""Save all scheduled tasks to JSON file."""
|
|
74
|
+
ensure_scheduler_dirs()
|
|
75
|
+
temp_path = Path(SCHEDULES_FILE).with_suffix(".tmp")
|
|
76
|
+
with open(temp_path, "w", encoding="utf-8") as f:
|
|
77
|
+
json.dump([t.to_dict() for t in tasks], f, indent=2, ensure_ascii=False)
|
|
78
|
+
temp_path.replace(SCHEDULES_FILE)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def add_task(task: ScheduledTask) -> None:
|
|
82
|
+
"""Add a new scheduled task."""
|
|
83
|
+
tasks = load_tasks()
|
|
84
|
+
tasks.append(task)
|
|
85
|
+
save_tasks(tasks)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def update_task(task: ScheduledTask) -> bool:
|
|
89
|
+
"""Update an existing task. Returns True if found and updated."""
|
|
90
|
+
tasks = load_tasks()
|
|
91
|
+
for i, t in enumerate(tasks):
|
|
92
|
+
if t.id == task.id:
|
|
93
|
+
tasks[i] = task
|
|
94
|
+
save_tasks(tasks)
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def delete_task(task_id: str) -> bool:
|
|
100
|
+
"""Delete a task by ID. Returns True if found and deleted."""
|
|
101
|
+
tasks = load_tasks()
|
|
102
|
+
original_len = len(tasks)
|
|
103
|
+
tasks = [t for t in tasks if t.id != task_id]
|
|
104
|
+
if len(tasks) < original_len:
|
|
105
|
+
save_tasks(tasks)
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_task(task_id: str) -> Optional[ScheduledTask]:
|
|
111
|
+
"""Get a task by ID."""
|
|
112
|
+
tasks = load_tasks()
|
|
113
|
+
for t in tasks:
|
|
114
|
+
if t.id == task_id:
|
|
115
|
+
return t
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def toggle_task(task_id: str) -> Optional[bool]:
|
|
120
|
+
"""Toggle a task's enabled state. Returns new state or None if not found."""
|
|
121
|
+
task = get_task(task_id)
|
|
122
|
+
if task:
|
|
123
|
+
task.enabled = not task.enabled
|
|
124
|
+
update_task(task)
|
|
125
|
+
return task.enabled
|
|
126
|
+
return None
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Scheduler daemon.
|
|
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
|