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,150 @@
1
+ import threading
2
+ from contextlib import asynccontextmanager, suppress
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, AsyncIterator, List
5
+
6
+ from pydantic_ai._run_context import RunContext
7
+ from pydantic_ai.models import (
8
+ Model,
9
+ ModelMessage,
10
+ ModelRequestParameters,
11
+ ModelResponse,
12
+ ModelSettings,
13
+ StreamedResponse,
14
+ )
15
+
16
+ try:
17
+ from opentelemetry.context import get_current_span
18
+ except ImportError:
19
+ # If opentelemetry is not installed, provide a dummy implementation
20
+ def get_current_span():
21
+ class DummySpan:
22
+ def is_recording(self):
23
+ return False
24
+
25
+ def set_attributes(self, attributes):
26
+ pass
27
+
28
+ return DummySpan()
29
+
30
+
31
+ @dataclass(init=False)
32
+ class RoundRobinModel(Model):
33
+ """A model that cycles through multiple models in a round-robin fashion.
34
+
35
+ This model distributes requests across multiple candidate models to help
36
+ overcome rate limits or distribute load.
37
+ """
38
+
39
+ models: List[Model]
40
+ _current_index: int = field(default=0, repr=False)
41
+ _model_name: str = field(repr=False)
42
+ _rotate_every: int = field(default=1, repr=False)
43
+ _request_count: int = field(default=0, repr=False)
44
+ _lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
45
+
46
+ def __init__(
47
+ self,
48
+ *models: Model,
49
+ rotate_every: int = 1,
50
+ settings: ModelSettings | None = None,
51
+ ):
52
+ """Initialize a round-robin model instance.
53
+
54
+ Args:
55
+ models: The model instances to cycle through.
56
+ rotate_every: Number of requests before rotating to the next model (default: 1).
57
+ settings: Model settings that will be used as defaults for this model.
58
+ """
59
+ super().__init__(settings=settings)
60
+ if not models:
61
+ raise ValueError("At least one model must be provided")
62
+ if rotate_every < 1:
63
+ raise ValueError("rotate_every must be at least 1")
64
+ self.models = list(models)
65
+ self._current_index = 0
66
+ self._request_count = 0
67
+ self._rotate_every = rotate_every
68
+ self._lock = threading.Lock()
69
+
70
+ @property
71
+ def model_name(self) -> str:
72
+ """The model name showing this is a round-robin model with its candidates."""
73
+ base_name = f"round_robin:{','.join(model.model_name for model in self.models)}"
74
+ if self._rotate_every != 1:
75
+ return f"{base_name}:rotate_every={self._rotate_every}"
76
+ return base_name
77
+
78
+ @property
79
+ def system(self) -> str:
80
+ """System prompt from the current model."""
81
+ return self.models[self._current_index].system
82
+
83
+ @property
84
+ def base_url(self) -> str | None:
85
+ """Base URL from the current model."""
86
+ return self.models[self._current_index].base_url
87
+
88
+ def _get_next_model(self) -> Model:
89
+ """Get the next model in the round-robin sequence and update the index."""
90
+ with self._lock:
91
+ model = self.models[self._current_index]
92
+ self._request_count += 1
93
+ if self._request_count >= self._rotate_every:
94
+ self._current_index = (self._current_index + 1) % len(self.models)
95
+ self._request_count = 0
96
+ return model
97
+
98
+ async def request(
99
+ self,
100
+ messages: list[ModelMessage],
101
+ model_settings: ModelSettings | None,
102
+ model_request_parameters: ModelRequestParameters,
103
+ ) -> ModelResponse:
104
+ """Make a request using the next model in the round-robin sequence."""
105
+ current_model = self._get_next_model()
106
+ # Use prepare_request to merge settings and customize parameters
107
+ merged_settings, prepared_params = current_model.prepare_request(
108
+ model_settings, model_request_parameters
109
+ )
110
+
111
+ try:
112
+ response = await current_model.request(
113
+ messages, merged_settings, prepared_params
114
+ )
115
+ self._set_span_attributes(current_model)
116
+ return response
117
+ except Exception:
118
+ # Unlike FallbackModel, we don't try other models here
119
+ # The round-robin strategy is about distribution, not failover
120
+ raise
121
+
122
+ @asynccontextmanager
123
+ async def request_stream(
124
+ self,
125
+ messages: list[ModelMessage],
126
+ model_settings: ModelSettings | None,
127
+ model_request_parameters: ModelRequestParameters,
128
+ run_context: RunContext[Any] | None = None,
129
+ ) -> AsyncIterator[StreamedResponse]:
130
+ """Make a streaming request using the next model in the round-robin sequence."""
131
+ current_model = self._get_next_model()
132
+ # Use prepare_request to merge settings and customize parameters
133
+ merged_settings, prepared_params = current_model.prepare_request(
134
+ model_settings, model_request_parameters
135
+ )
136
+
137
+ async with current_model.request_stream(
138
+ messages, merged_settings, prepared_params, run_context
139
+ ) as response:
140
+ self._set_span_attributes(current_model)
141
+ yield response
142
+
143
+ def _set_span_attributes(self, model: Model):
144
+ """Set span attributes for observability."""
145
+ with suppress(Exception):
146
+ span = get_current_span()
147
+ if span.is_recording():
148
+ attributes = getattr(span, "attributes", {})
149
+ if attributes.get("gen_ai.request.model") == self.model_name:
150
+ span.set_attributes(model.model_attributes(model))
@@ -0,0 +1,41 @@
1
+ """Code Puppy Scheduler - Run scheduled prompts automatically.
2
+
3
+ This module provides a cross-platform scheduler daemon that executes
4
+ Code Puppy 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 Code Puppy 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 Code Puppy 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