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.
Files changed (289) 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 +147 -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 +630 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +122 -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 +380 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +167 -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 +2145 -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 +296 -0
  28. code_puppy/agents/pack/husky.py +307 -0
  29. code_puppy/agents/pack/retriever.py +380 -0
  30. code_puppy/agents/pack/shepherd.py +327 -0
  31. code_puppy/agents/pack/terrier.py +281 -0
  32. code_puppy/agents/pack/watchdog.py +357 -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 +674 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +664 -0
  49. code_puppy/cli_runner.py +1038 -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 +526 -0
  57. code_puppy/command_line/command_handler.py +283 -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 +853 -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 +91 -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/skills_completion.py +160 -0
  97. code_puppy/command_line/uc_menu.py +893 -0
  98. code_puppy/command_line/utils.py +93 -0
  99. code_puppy/command_line/wiggum_state.py +78 -0
  100. code_puppy/config.py +1787 -0
  101. code_puppy/error_logging.py +133 -0
  102. code_puppy/gemini_code_assist.py +385 -0
  103. code_puppy/gemini_model.py +754 -0
  104. code_puppy/hook_engine/README.md +105 -0
  105. code_puppy/hook_engine/__init__.py +15 -0
  106. code_puppy/hook_engine/aliases.py +155 -0
  107. code_puppy/hook_engine/engine.py +195 -0
  108. code_puppy/hook_engine/executor.py +293 -0
  109. code_puppy/hook_engine/matcher.py +145 -0
  110. code_puppy/hook_engine/models.py +222 -0
  111. code_puppy/hook_engine/registry.py +106 -0
  112. code_puppy/hook_engine/validator.py +141 -0
  113. code_puppy/http_utils.py +361 -0
  114. code_puppy/keymap.py +128 -0
  115. code_puppy/main.py +10 -0
  116. code_puppy/mcp_/__init__.py +66 -0
  117. code_puppy/mcp_/async_lifecycle.py +286 -0
  118. code_puppy/mcp_/blocking_startup.py +469 -0
  119. code_puppy/mcp_/captured_stdio_server.py +275 -0
  120. code_puppy/mcp_/circuit_breaker.py +290 -0
  121. code_puppy/mcp_/config_wizard.py +507 -0
  122. code_puppy/mcp_/dashboard.py +308 -0
  123. code_puppy/mcp_/error_isolation.py +407 -0
  124. code_puppy/mcp_/examples/retry_example.py +226 -0
  125. code_puppy/mcp_/health_monitor.py +589 -0
  126. code_puppy/mcp_/managed_server.py +428 -0
  127. code_puppy/mcp_/manager.py +807 -0
  128. code_puppy/mcp_/mcp_logs.py +224 -0
  129. code_puppy/mcp_/registry.py +451 -0
  130. code_puppy/mcp_/retry_manager.py +337 -0
  131. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  132. code_puppy/mcp_/status_tracker.py +355 -0
  133. code_puppy/mcp_/system_tools.py +209 -0
  134. code_puppy/mcp_prompts/__init__.py +1 -0
  135. code_puppy/mcp_prompts/hook_creator.py +103 -0
  136. code_puppy/messaging/__init__.py +255 -0
  137. code_puppy/messaging/bus.py +613 -0
  138. code_puppy/messaging/commands.py +167 -0
  139. code_puppy/messaging/markdown_patches.py +57 -0
  140. code_puppy/messaging/message_queue.py +361 -0
  141. code_puppy/messaging/messages.py +569 -0
  142. code_puppy/messaging/queue_console.py +271 -0
  143. code_puppy/messaging/renderers.py +311 -0
  144. code_puppy/messaging/rich_renderer.py +1153 -0
  145. code_puppy/messaging/spinner/__init__.py +83 -0
  146. code_puppy/messaging/spinner/console_spinner.py +240 -0
  147. code_puppy/messaging/spinner/spinner_base.py +96 -0
  148. code_puppy/messaging/subagent_console.py +460 -0
  149. code_puppy/model_factory.py +848 -0
  150. code_puppy/model_switching.py +63 -0
  151. code_puppy/model_utils.py +168 -0
  152. code_puppy/models.json +130 -0
  153. code_puppy/models_dev_api.json +1 -0
  154. code_puppy/models_dev_parser.py +592 -0
  155. code_puppy/plugins/__init__.py +186 -0
  156. code_puppy/plugins/agent_skills/__init__.py +22 -0
  157. code_puppy/plugins/agent_skills/config.py +175 -0
  158. code_puppy/plugins/agent_skills/discovery.py +136 -0
  159. code_puppy/plugins/agent_skills/downloader.py +392 -0
  160. code_puppy/plugins/agent_skills/installer.py +22 -0
  161. code_puppy/plugins/agent_skills/metadata.py +219 -0
  162. code_puppy/plugins/agent_skills/prompt_builder.py +100 -0
  163. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  164. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  165. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  166. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  167. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  168. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  169. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  170. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  171. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  172. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  173. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  174. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  175. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  176. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  177. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  178. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  179. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  180. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  181. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  182. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  183. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  184. code_puppy/plugins/chatgpt_oauth/test_plugin.py +295 -0
  185. code_puppy/plugins/chatgpt_oauth/utils.py +499 -0
  186. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  187. code_puppy/plugins/claude_code_hooks/config.py +131 -0
  188. code_puppy/plugins/claude_code_hooks/register_callbacks.py +163 -0
  189. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  190. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  191. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  192. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  193. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  194. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  195. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  196. code_puppy/plugins/claude_code_oauth/utils.py +601 -0
  197. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  198. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  199. code_puppy/plugins/example_custom_command/README.md +280 -0
  200. code_puppy/plugins/example_custom_command/register_callbacks.py +48 -0
  201. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  202. code_puppy/plugins/file_permission_handler/register_callbacks.py +528 -0
  203. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  204. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  205. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  206. code_puppy/plugins/hook_creator/__init__.py +1 -0
  207. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  208. code_puppy/plugins/hook_manager/__init__.py +1 -0
  209. code_puppy/plugins/hook_manager/config.py +277 -0
  210. code_puppy/plugins/hook_manager/hooks_menu.py +551 -0
  211. code_puppy/plugins/hook_manager/register_callbacks.py +205 -0
  212. code_puppy/plugins/oauth_puppy_html.py +224 -0
  213. code_puppy/plugins/scheduler/__init__.py +1 -0
  214. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  215. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  216. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  217. code_puppy/plugins/shell_safety/__init__.py +6 -0
  218. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  219. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  220. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  221. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  222. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  223. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  224. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  225. code_puppy/plugins/universal_constructor/models.py +138 -0
  226. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  227. code_puppy/plugins/universal_constructor/registry.py +302 -0
  228. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  229. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  230. code_puppy/pydantic_patches.py +317 -0
  231. code_puppy/reopenable_async_client.py +232 -0
  232. code_puppy/round_robin_model.py +150 -0
  233. code_puppy/scheduler/__init__.py +41 -0
  234. code_puppy/scheduler/__main__.py +9 -0
  235. code_puppy/scheduler/cli.py +118 -0
  236. code_puppy/scheduler/config.py +126 -0
  237. code_puppy/scheduler/daemon.py +280 -0
  238. code_puppy/scheduler/executor.py +155 -0
  239. code_puppy/scheduler/platform.py +19 -0
  240. code_puppy/scheduler/platform_unix.py +22 -0
  241. code_puppy/scheduler/platform_win.py +32 -0
  242. code_puppy/session_storage.py +338 -0
  243. code_puppy/status_display.py +257 -0
  244. code_puppy/summarization_agent.py +176 -0
  245. code_puppy/terminal_utils.py +418 -0
  246. code_puppy/tools/__init__.py +470 -0
  247. code_puppy/tools/agent_tools.py +616 -0
  248. code_puppy/tools/ask_user_question/__init__.py +26 -0
  249. code_puppy/tools/ask_user_question/constants.py +73 -0
  250. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  251. code_puppy/tools/ask_user_question/handler.py +232 -0
  252. code_puppy/tools/ask_user_question/models.py +304 -0
  253. code_puppy/tools/ask_user_question/registration.py +36 -0
  254. code_puppy/tools/ask_user_question/renderers.py +309 -0
  255. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  256. code_puppy/tools/ask_user_question/theme.py +155 -0
  257. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  258. code_puppy/tools/browser/__init__.py +37 -0
  259. code_puppy/tools/browser/browser_control.py +289 -0
  260. code_puppy/tools/browser/browser_interactions.py +545 -0
  261. code_puppy/tools/browser/browser_locators.py +640 -0
  262. code_puppy/tools/browser/browser_manager.py +378 -0
  263. code_puppy/tools/browser/browser_navigation.py +251 -0
  264. code_puppy/tools/browser/browser_screenshot.py +179 -0
  265. code_puppy/tools/browser/browser_scripts.py +462 -0
  266. code_puppy/tools/browser/browser_workflows.py +221 -0
  267. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  268. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  269. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  270. code_puppy/tools/browser/terminal_tools.py +525 -0
  271. code_puppy/tools/command_runner.py +1346 -0
  272. code_puppy/tools/common.py +1409 -0
  273. code_puppy/tools/display.py +84 -0
  274. code_puppy/tools/file_modifications.py +739 -0
  275. code_puppy/tools/file_operations.py +802 -0
  276. code_puppy/tools/scheduler_tools.py +412 -0
  277. code_puppy/tools/skills_tools.py +251 -0
  278. code_puppy/tools/subagent_context.py +158 -0
  279. code_puppy/tools/tools_content.py +51 -0
  280. code_puppy/tools/universal_constructor.py +889 -0
  281. code_puppy/uvx_detection.py +242 -0
  282. code_puppy/version_checker.py +82 -0
  283. newcode-0.1.1.data/data/code_puppy/models.json +130 -0
  284. newcode-0.1.1.data/data/code_puppy/models_dev_api.json +1 -0
  285. newcode-0.1.1.dist-info/METADATA +154 -0
  286. newcode-0.1.1.dist-info/RECORD +289 -0
  287. newcode-0.1.1.dist-info/WHEEL +4 -0
  288. newcode-0.1.1.dist-info/entry_points.txt +3 -0
  289. 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,9 @@
1
+ """Entry point for running scheduler daemon directly.
2
+
3
+ Usage: python -m code_puppy.scheduler
4
+ """
5
+
6
+ from code_puppy.scheduler.daemon import start_daemon
7
+
8
+ if __name__ == "__main__":
9
+ start_daemon(foreground=True)
@@ -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