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.
Files changed (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. 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