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,155 @@
1
+ """Task executor for the 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
@@ -0,0 +1,338 @@
1
+ """Shared helpers for persisting and restoring chat sessions.
2
+
3
+ This module centralises the pickle + metadata handling that used to live in
4
+ both the CLI command handler and the auto-save feature. Keeping it here helps
5
+ us avoid duplication while staying inside the Zen-of-Python sweet spot: simple
6
+ is better than complex, nested side effects are worse than deliberate helpers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import pickle
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any, Callable, List
16
+
17
+
18
+ def _safe_loads(data: bytes) -> Any:
19
+ """Deserialize pickle data."""
20
+ return pickle.loads(data) # noqa: S301
21
+
22
+
23
+ _LEGACY_SIGNED_HEADER = b"CPSESSION\x01"
24
+ _LEGACY_SIGNATURE_SIZE = (
25
+ 32 # legacy signature bytes, retained only for backward-compat parsing
26
+ )
27
+
28
+ SessionHistory = List[Any]
29
+ TokenEstimator = Callable[[Any], int]
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class SessionPaths:
34
+ pickle_path: Path
35
+ metadata_path: Path
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class SessionMetadata:
40
+ session_name: str
41
+ timestamp: str
42
+ message_count: int
43
+ total_tokens: int
44
+ pickle_path: Path
45
+ metadata_path: Path
46
+ auto_saved: bool = False
47
+
48
+ def as_serialisable(self) -> dict[str, Any]:
49
+ return {
50
+ "session_name": self.session_name,
51
+ "timestamp": self.timestamp,
52
+ "message_count": self.message_count,
53
+ "total_tokens": self.total_tokens,
54
+ "file_path": str(self.pickle_path),
55
+ "auto_saved": self.auto_saved,
56
+ }
57
+
58
+
59
+ def _extract_pickle_payload(raw: bytes) -> bytes:
60
+ """Return the pickle payload from raw session file bytes.
61
+
62
+ New format is raw pickle bytes.
63
+ Legacy format was: header + 32-byte signature + pickle payload.
64
+ We no longer verify or generate signatures.
65
+ """
66
+ if raw.startswith(_LEGACY_SIGNED_HEADER):
67
+ offset = len(_LEGACY_SIGNED_HEADER) + _LEGACY_SIGNATURE_SIZE
68
+ return raw[offset:]
69
+ return raw
70
+
71
+
72
+ def ensure_directory(path: Path) -> Path:
73
+ path.mkdir(parents=True, exist_ok=True)
74
+ return path
75
+
76
+
77
+ def build_session_paths(base_dir: Path, session_name: str) -> SessionPaths:
78
+ pickle_path = base_dir / f"{session_name}.pkl"
79
+ metadata_path = base_dir / f"{session_name}_meta.json"
80
+ return SessionPaths(pickle_path=pickle_path, metadata_path=metadata_path)
81
+
82
+
83
+ def save_session(
84
+ *,
85
+ history: SessionHistory,
86
+ session_name: str,
87
+ base_dir: Path,
88
+ timestamp: str,
89
+ token_estimator: TokenEstimator,
90
+ auto_saved: bool = False,
91
+ ) -> SessionMetadata:
92
+ ensure_directory(base_dir)
93
+ paths = build_session_paths(base_dir, session_name)
94
+
95
+ pickle_data = pickle.dumps(history)
96
+ tmp_pickle = paths.pickle_path.with_suffix(".tmp")
97
+ with tmp_pickle.open("wb") as pickle_file:
98
+ pickle_file.write(pickle_data)
99
+ tmp_pickle.replace(paths.pickle_path)
100
+
101
+ total_tokens = sum(token_estimator(message) for message in history)
102
+ metadata = SessionMetadata(
103
+ session_name=session_name,
104
+ timestamp=timestamp,
105
+ message_count=len(history),
106
+ total_tokens=total_tokens,
107
+ pickle_path=paths.pickle_path,
108
+ metadata_path=paths.metadata_path,
109
+ auto_saved=auto_saved,
110
+ )
111
+
112
+ tmp_metadata = paths.metadata_path.with_suffix(".tmp")
113
+ with tmp_metadata.open("w", encoding="utf-8") as metadata_file:
114
+ json.dump(metadata.as_serialisable(), metadata_file, indent=2)
115
+ tmp_metadata.replace(paths.metadata_path)
116
+
117
+ return metadata
118
+
119
+
120
+ def load_session(
121
+ session_name: str, base_dir: Path, *, allow_legacy: bool = False
122
+ ) -> SessionHistory:
123
+ # Kept for API compatibility; legacy loading is always supported now.
124
+ _ = allow_legacy
125
+
126
+ paths = build_session_paths(base_dir, session_name)
127
+ if not paths.pickle_path.exists():
128
+ raise FileNotFoundError(paths.pickle_path)
129
+
130
+ raw = paths.pickle_path.read_bytes()
131
+ pickle_data = _extract_pickle_payload(raw)
132
+ return _safe_loads(pickle_data)
133
+
134
+
135
+ def list_sessions(base_dir: Path) -> List[str]:
136
+ if not base_dir.exists():
137
+ return []
138
+ return sorted(path.stem for path in base_dir.glob("*.pkl"))
139
+
140
+
141
+ def cleanup_sessions(base_dir: Path, max_sessions: int) -> List[str]:
142
+ if max_sessions <= 0:
143
+ return []
144
+
145
+ if not base_dir.exists():
146
+ return []
147
+
148
+ candidate_paths = list(base_dir.glob("*.pkl"))
149
+ if len(candidate_paths) <= max_sessions:
150
+ return []
151
+
152
+ sorted_candidates = sorted(
153
+ ((path.stat().st_mtime, path) for path in candidate_paths),
154
+ key=lambda item: item[0],
155
+ )
156
+
157
+ stale_entries = sorted_candidates[:-max_sessions]
158
+ removed_sessions: List[str] = []
159
+ for _, pickle_path in stale_entries:
160
+ metadata_path = base_dir / f"{pickle_path.stem}_meta.json"
161
+ try:
162
+ pickle_path.unlink(missing_ok=True)
163
+ metadata_path.unlink(missing_ok=True)
164
+ removed_sessions.append(pickle_path.stem)
165
+ except OSError:
166
+ continue
167
+
168
+ return removed_sessions
169
+
170
+
171
+ async def restore_autosave_interactively(base_dir: Path) -> None:
172
+ """Prompt the user to load an autosave session from base_dir, if any exist.
173
+
174
+ This helper is deliberately placed in session_storage to keep autosave
175
+ restoration close to the persistence layer. It uses the same public APIs
176
+ (list_sessions, load_session) and mirrors the interactive behaviours from
177
+ the command handler.
178
+ """
179
+ sessions = list_sessions(base_dir)
180
+ if not sessions:
181
+ return
182
+
183
+ # Import locally to avoid pulling the messaging layer into storage modules
184
+ from datetime import datetime
185
+
186
+ from prompt_toolkit.formatted_text import FormattedText
187
+
188
+ from code_puppy.agents.agent_manager import get_current_agent
189
+ from code_puppy.command_line.prompt_toolkit_completion import (
190
+ get_input_with_combined_completion,
191
+ )
192
+ from code_puppy.messaging import emit_success, emit_system_message, emit_warning
193
+
194
+ entries = []
195
+ for name in sessions:
196
+ meta_path = base_dir / f"{name}_meta.json"
197
+ try:
198
+ with meta_path.open("r", encoding="utf-8") as meta_file:
199
+ data = json.load(meta_file)
200
+ timestamp = data.get("timestamp")
201
+ message_count = data.get("message_count")
202
+ except Exception:
203
+ timestamp = None
204
+ message_count = None
205
+ entries.append((name, timestamp, message_count))
206
+
207
+ def sort_key(entry):
208
+ _, timestamp, _ = entry
209
+ if timestamp:
210
+ try:
211
+ return datetime.fromisoformat(timestamp)
212
+ except ValueError:
213
+ return datetime.min
214
+ return datetime.min
215
+
216
+ entries.sort(key=sort_key, reverse=True)
217
+
218
+ PAGE_SIZE = 5
219
+ total = len(entries)
220
+ page = 0
221
+
222
+ def render_page() -> None:
223
+ start = page * PAGE_SIZE
224
+ end = min(start + PAGE_SIZE, total)
225
+ page_entries = entries[start:end]
226
+ emit_system_message("Autosave Sessions Available:")
227
+ for idx, (name, timestamp, message_count) in enumerate(page_entries, start=1):
228
+ timestamp_display = timestamp or "unknown time"
229
+ message_display = (
230
+ f"{message_count} messages"
231
+ if message_count is not None
232
+ else "unknown size"
233
+ )
234
+ emit_system_message(
235
+ f" [{idx}] {name} ({message_display}, saved at {timestamp_display})"
236
+ )
237
+ # If there are more pages, offer next-page; show 'Return to first page' on last page
238
+ if total > PAGE_SIZE:
239
+ page_count = (total + PAGE_SIZE - 1) // PAGE_SIZE
240
+ is_last_page = (page + 1) >= page_count
241
+ remaining = total - (page * PAGE_SIZE + len(page_entries))
242
+ summary = (
243
+ f" and {remaining} more" if (remaining > 0 and not is_last_page) else ""
244
+ )
245
+ label = "Return to first page" if is_last_page else f"Next page{summary}"
246
+ emit_system_message(f" [6] {label}")
247
+ emit_system_message(" [Enter] Skip loading autosave")
248
+
249
+ chosen_name: str | None = None
250
+
251
+ while True:
252
+ render_page()
253
+ try:
254
+ selection = await get_input_with_combined_completion(
255
+ FormattedText(
256
+ [
257
+ (
258
+ "class:prompt",
259
+ "Pick 1-5 to load, 6 for next, or name/Enter: ",
260
+ )
261
+ ]
262
+ )
263
+ )
264
+ except (KeyboardInterrupt, EOFError):
265
+ emit_warning("Autosave selection cancelled")
266
+ return
267
+
268
+ selection = (selection or "").strip()
269
+ if not selection:
270
+ return
271
+
272
+ # Numeric choice: 1-5 select within current page; 6 advances page
273
+ if selection.isdigit():
274
+ num = int(selection)
275
+ if num == 6 and total > PAGE_SIZE:
276
+ page = (page + 1) % ((total + PAGE_SIZE - 1) // PAGE_SIZE)
277
+ # loop and re-render next page
278
+ continue
279
+ if 1 <= num <= 5:
280
+ start = page * PAGE_SIZE
281
+ idx = start + (num - 1)
282
+ if 0 <= idx < total:
283
+ chosen_name = entries[idx][0]
284
+ break
285
+ else:
286
+ emit_warning("Invalid selection for this page")
287
+ continue
288
+ emit_warning("Invalid selection; choose 1-5 or 6 for next")
289
+ continue
290
+
291
+ # Allow direct typing by exact session name
292
+ for name, _ts, _mc in entries:
293
+ if name == selection:
294
+ chosen_name = name
295
+ break
296
+ if chosen_name:
297
+ break
298
+ emit_warning("No autosave loaded (invalid selection)")
299
+ # keep looping and allow another try
300
+
301
+ if not chosen_name:
302
+ return
303
+
304
+ try:
305
+ history = load_session(chosen_name, base_dir)
306
+ except FileNotFoundError:
307
+ emit_warning(f"Autosave '{chosen_name}' could not be found")
308
+ return
309
+ except Exception as exc:
310
+ emit_warning(f"Failed to load autosave '{chosen_name}': {exc}")
311
+ return
312
+
313
+ agent = get_current_agent()
314
+ agent.set_message_history(history)
315
+
316
+ # Set current autosave session id so subsequent autosaves overwrite this session
317
+ try:
318
+ from code_puppy.config import set_current_autosave_from_session_name
319
+
320
+ set_current_autosave_from_session_name(chosen_name)
321
+ except Exception:
322
+ pass
323
+
324
+ total_tokens = sum(agent.estimate_tokens_for_message(msg) for msg in history)
325
+
326
+ session_path = base_dir / f"{chosen_name}.pkl"
327
+ emit_success(
328
+ f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
329
+ f"📁 From: {session_path}"
330
+ )
331
+
332
+ # Display recent message history for context
333
+ try:
334
+ from code_puppy.command_line.autosave_menu import display_resumed_history
335
+
336
+ display_resumed_history(history)
337
+ except Exception:
338
+ pass # Don't fail if display doesn't work in non-TTY environment