newcode 0.2.5__tar.gz → 0.2.6__tar.gz

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 (287) hide show
  1. {newcode-0.2.5 → newcode-0.2.6}/PKG-INFO +2 -3
  2. {newcode-0.2.5 → newcode-0.2.6}/README.md +1 -2
  3. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/base_agent.py +178 -94
  4. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/attachments.py +4 -0
  5. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/clipboard.py +5 -1
  6. newcode-0.2.6/newcode/image_utils.py +85 -0
  7. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/register_callbacks.py +51 -0
  8. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/token_refresh_heartbeat.py +4 -0
  9. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/__init__.py +0 -23
  10. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/browser_screenshot.py +5 -1
  11. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/terminal_screenshot_tools.py +6 -2
  12. {newcode-0.2.5 → newcode-0.2.6}/pyproject.toml +1 -1
  13. newcode-0.2.5/newcode/agents/agent_scheduler.py +0 -120
  14. newcode-0.2.5/newcode/plugins/scheduler/__init__.py +0 -1
  15. newcode-0.2.5/newcode/plugins/scheduler/register_callbacks.py +0 -88
  16. newcode-0.2.5/newcode/plugins/scheduler/scheduler_menu.py +0 -520
  17. newcode-0.2.5/newcode/plugins/scheduler/scheduler_wizard.py +0 -341
  18. newcode-0.2.5/newcode/scheduler/__init__.py +0 -41
  19. newcode-0.2.5/newcode/scheduler/__main__.py +0 -9
  20. newcode-0.2.5/newcode/scheduler/cli.py +0 -118
  21. newcode-0.2.5/newcode/scheduler/config.py +0 -126
  22. newcode-0.2.5/newcode/scheduler/daemon.py +0 -280
  23. newcode-0.2.5/newcode/scheduler/executor.py +0 -155
  24. newcode-0.2.5/newcode/scheduler/platform.py +0 -19
  25. newcode-0.2.5/newcode/scheduler/platform_unix.py +0 -22
  26. newcode-0.2.5/newcode/scheduler/platform_win.py +0 -32
  27. newcode-0.2.5/newcode/tools/scheduler_tools.py +0 -412
  28. {newcode-0.2.5 → newcode-0.2.6}/.gitignore +0 -0
  29. {newcode-0.2.5 → newcode-0.2.6}/LICENSE +0 -0
  30. {newcode-0.2.5 → newcode-0.2.6}/newcode/__init__.py +0 -0
  31. {newcode-0.2.5 → newcode-0.2.6}/newcode/__main__.py +0 -0
  32. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/__init__.py +0 -0
  33. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_c_reviewer.py +0 -0
  34. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_code_agent.py +0 -0
  35. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_code_reviewer.py +0 -0
  36. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_cpp_reviewer.py +0 -0
  37. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_creator_agent.py +0 -0
  38. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_golang_reviewer.py +0 -0
  39. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_javascript_reviewer.py +0 -0
  40. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_manager.py +0 -0
  41. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_pack_leader.py +0 -0
  42. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_planning.py +0 -0
  43. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_python_programmer.py +0 -0
  44. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_python_reviewer.py +0 -0
  45. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_qa_browser.py +0 -0
  46. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_qa_expert.py +0 -0
  47. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_security_auditor.py +0 -0
  48. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_terminal_qa.py +0 -0
  49. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/agent_typescript_reviewer.py +0 -0
  50. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/event_stream_handler.py +0 -0
  51. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/json_agent.py +0 -0
  52. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/pack/__init__.py +0 -0
  53. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/pack/bloodhound.py +0 -0
  54. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/pack/husky.py +0 -0
  55. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/pack/retriever.py +0 -0
  56. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/pack/shepherd.py +0 -0
  57. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/pack/terrier.py +0 -0
  58. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/pack/watchdog.py +0 -0
  59. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/prompt_reviewer.py +0 -0
  60. {newcode-0.2.5 → newcode-0.2.6}/newcode/agents/subagent_stream_handler.py +0 -0
  61. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/__init__.py +0 -0
  62. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/app.py +0 -0
  63. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/main.py +0 -0
  64. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/pty_manager.py +0 -0
  65. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/routers/__init__.py +0 -0
  66. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/routers/agents.py +0 -0
  67. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/routers/commands.py +0 -0
  68. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/routers/config.py +0 -0
  69. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/routers/sessions.py +0 -0
  70. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/templates/terminal.html +0 -0
  71. {newcode-0.2.5 → newcode-0.2.6}/newcode/api/websocket.py +0 -0
  72. {newcode-0.2.5 → newcode-0.2.6}/newcode/callbacks.py +0 -0
  73. {newcode-0.2.5 → newcode-0.2.6}/newcode/chatgpt_codex_client.py +0 -0
  74. {newcode-0.2.5 → newcode-0.2.6}/newcode/claude_cache_client.py +0 -0
  75. {newcode-0.2.5 → newcode-0.2.6}/newcode/cli_runner.py +0 -0
  76. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/__init__.py +0 -0
  77. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/add_model_menu.py +0 -0
  78. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/agent_menu.py +0 -0
  79. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/autosave_menu.py +0 -0
  80. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/colors_menu.py +0 -0
  81. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/command_handler.py +0 -0
  82. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/command_registry.py +0 -0
  83. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/config_commands.py +0 -0
  84. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/core_commands.py +0 -0
  85. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/diff_menu.py +0 -0
  86. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/file_path_completion.py +0 -0
  87. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/load_context_completion.py +0 -0
  88. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/__init__.py +0 -0
  89. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/base.py +0 -0
  90. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/catalog_server_installer.py +0 -0
  91. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/custom_server_form.py +0 -0
  92. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/custom_server_installer.py +0 -0
  93. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/edit_command.py +0 -0
  94. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/handler.py +0 -0
  95. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/help_command.py +0 -0
  96. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/install_command.py +0 -0
  97. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/install_menu.py +0 -0
  98. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/list_command.py +0 -0
  99. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/logs_command.py +0 -0
  100. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/remove_command.py +0 -0
  101. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/restart_command.py +0 -0
  102. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/search_command.py +0 -0
  103. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/start_all_command.py +0 -0
  104. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/start_command.py +0 -0
  105. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/status_command.py +0 -0
  106. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/stop_all_command.py +0 -0
  107. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/stop_command.py +0 -0
  108. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/test_command.py +0 -0
  109. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/utils.py +0 -0
  110. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp/wizard_utils.py +0 -0
  111. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/mcp_completion.py +0 -0
  112. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/model_picker_completion.py +0 -0
  113. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/model_settings_menu.py +0 -0
  114. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/motd.py +0 -0
  115. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/onboarding_slides.py +0 -0
  116. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/onboarding_wizard.py +0 -0
  117. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/pin_command_completion.py +0 -0
  118. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/prompt_toolkit_completion.py +0 -0
  119. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/session_commands.py +0 -0
  120. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/skills_completion.py +0 -0
  121. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/uc_menu.py +0 -0
  122. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/utils.py +0 -0
  123. {newcode-0.2.5 → newcode-0.2.6}/newcode/command_line/wiggum_state.py +0 -0
  124. {newcode-0.2.5 → newcode-0.2.6}/newcode/config.py +0 -0
  125. {newcode-0.2.5 → newcode-0.2.6}/newcode/error_logging.py +0 -0
  126. {newcode-0.2.5 → newcode-0.2.6}/newcode/gemini_code_assist.py +0 -0
  127. {newcode-0.2.5 → newcode-0.2.6}/newcode/gemini_model.py +0 -0
  128. {newcode-0.2.5 → newcode-0.2.6}/newcode/hook_engine/README.md +0 -0
  129. {newcode-0.2.5 → newcode-0.2.6}/newcode/hook_engine/__init__.py +0 -0
  130. {newcode-0.2.5 → newcode-0.2.6}/newcode/hook_engine/aliases.py +0 -0
  131. {newcode-0.2.5 → newcode-0.2.6}/newcode/hook_engine/engine.py +0 -0
  132. {newcode-0.2.5 → newcode-0.2.6}/newcode/hook_engine/executor.py +0 -0
  133. {newcode-0.2.5 → newcode-0.2.6}/newcode/hook_engine/matcher.py +0 -0
  134. {newcode-0.2.5 → newcode-0.2.6}/newcode/hook_engine/models.py +0 -0
  135. {newcode-0.2.5 → newcode-0.2.6}/newcode/hook_engine/registry.py +0 -0
  136. {newcode-0.2.5 → newcode-0.2.6}/newcode/hook_engine/validator.py +0 -0
  137. {newcode-0.2.5 → newcode-0.2.6}/newcode/http_utils.py +0 -0
  138. {newcode-0.2.5 → newcode-0.2.6}/newcode/keymap.py +0 -0
  139. {newcode-0.2.5 → newcode-0.2.6}/newcode/main.py +0 -0
  140. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/__init__.py +0 -0
  141. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/async_lifecycle.py +0 -0
  142. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/blocking_startup.py +0 -0
  143. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/captured_stdio_server.py +0 -0
  144. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/circuit_breaker.py +0 -0
  145. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/config_wizard.py +0 -0
  146. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/dashboard.py +0 -0
  147. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/error_isolation.py +0 -0
  148. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/examples/retry_example.py +0 -0
  149. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/health_monitor.py +0 -0
  150. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/managed_server.py +0 -0
  151. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/manager.py +0 -0
  152. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/mcp_logs.py +0 -0
  153. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/registry.py +0 -0
  154. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/retry_manager.py +0 -0
  155. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/server_registry_catalog.py +0 -0
  156. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/status_tracker.py +0 -0
  157. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_/system_tools.py +0 -0
  158. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_prompts/__init__.py +0 -0
  159. {newcode-0.2.5 → newcode-0.2.6}/newcode/mcp_prompts/hook_creator.py +0 -0
  160. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/__init__.py +0 -0
  161. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/bus.py +0 -0
  162. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/commands.py +0 -0
  163. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/markdown_patches.py +0 -0
  164. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/message_queue.py +0 -0
  165. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/messages.py +0 -0
  166. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/queue_console.py +0 -0
  167. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/renderers.py +0 -0
  168. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/rich_renderer.py +0 -0
  169. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/spinner/__init__.py +0 -0
  170. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/spinner/console_spinner.py +0 -0
  171. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/spinner/spinner_base.py +0 -0
  172. {newcode-0.2.5 → newcode-0.2.6}/newcode/messaging/subagent_console.py +0 -0
  173. {newcode-0.2.5 → newcode-0.2.6}/newcode/model_factory.py +0 -0
  174. {newcode-0.2.5 → newcode-0.2.6}/newcode/model_switching.py +0 -0
  175. {newcode-0.2.5 → newcode-0.2.6}/newcode/model_utils.py +0 -0
  176. {newcode-0.2.5 → newcode-0.2.6}/newcode/models.json +0 -0
  177. {newcode-0.2.5 → newcode-0.2.6}/newcode/models_dev_api.json +0 -0
  178. {newcode-0.2.5 → newcode-0.2.6}/newcode/models_dev_parser.py +0 -0
  179. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/__init__.py +0 -0
  180. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/__init__.py +0 -0
  181. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/config.py +0 -0
  182. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/discovery.py +0 -0
  183. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/downloader.py +0 -0
  184. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/installer.py +0 -0
  185. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/metadata.py +0 -0
  186. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/prompt_builder.py +0 -0
  187. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/register_callbacks.py +0 -0
  188. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/remote_catalog.py +0 -0
  189. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/skill_catalog.py +0 -0
  190. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/skills_install_menu.py +0 -0
  191. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/agent_skills/skills_menu.py +0 -0
  192. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/__init__.py +0 -0
  193. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/accounts.py +0 -0
  194. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/antigravity_model.py +0 -0
  195. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/config.py +0 -0
  196. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/constants.py +0 -0
  197. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/oauth.py +0 -0
  198. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/register_callbacks.py +0 -0
  199. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/storage.py +0 -0
  200. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/test_plugin.py +0 -0
  201. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/token.py +0 -0
  202. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/transport.py +0 -0
  203. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/utils.py +0 -0
  204. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/__init__.py +0 -0
  205. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/config.py +0 -0
  206. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  207. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  208. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/test_plugin.py +0 -0
  209. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/utils.py +0 -0
  210. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_hooks/__init__.py +0 -0
  211. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_hooks/config.py +0 -0
  212. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_hooks/register_callbacks.py +0 -0
  213. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/README.md +0 -0
  214. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/SETUP.md +0 -0
  215. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/__init__.py +0 -0
  216. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/config.py +0 -0
  217. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/test_plugin.py +0 -0
  218. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/utils.py +0 -0
  219. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/customizable_commands/__init__.py +0 -0
  220. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/customizable_commands/register_callbacks.py +0 -0
  221. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/example_custom_command/README.md +0 -0
  222. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/example_custom_command/register_callbacks.py +0 -0
  223. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/file_permission_handler/__init__.py +0 -0
  224. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/file_permission_handler/register_callbacks.py +0 -0
  225. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/frontend_emitter/__init__.py +0 -0
  226. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/frontend_emitter/emitter.py +0 -0
  227. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/frontend_emitter/register_callbacks.py +0 -0
  228. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/hook_creator/__init__.py +0 -0
  229. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/hook_creator/register_callbacks.py +0 -0
  230. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/hook_manager/__init__.py +0 -0
  231. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/hook_manager/config.py +0 -0
  232. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/hook_manager/hooks_menu.py +0 -0
  233. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/hook_manager/register_callbacks.py +0 -0
  234. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/oauth_puppy_html.py +0 -0
  235. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/shell_safety/__init__.py +0 -0
  236. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/shell_safety/agent_shell_safety.py +0 -0
  237. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/shell_safety/command_cache.py +0 -0
  238. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/shell_safety/register_callbacks.py +0 -0
  239. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/synthetic_status/__init__.py +0 -0
  240. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/synthetic_status/register_callbacks.py +0 -0
  241. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/synthetic_status/status_api.py +0 -0
  242. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/universal_constructor/__init__.py +0 -0
  243. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/universal_constructor/models.py +0 -0
  244. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/universal_constructor/register_callbacks.py +0 -0
  245. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/universal_constructor/registry.py +0 -0
  246. {newcode-0.2.5 → newcode-0.2.6}/newcode/plugins/universal_constructor/sandbox.py +0 -0
  247. {newcode-0.2.5 → newcode-0.2.6}/newcode/prompts/antigravity_system_prompt.md +0 -0
  248. {newcode-0.2.5 → newcode-0.2.6}/newcode/pydantic_patches.py +0 -0
  249. {newcode-0.2.5 → newcode-0.2.6}/newcode/reopenable_async_client.py +0 -0
  250. {newcode-0.2.5 → newcode-0.2.6}/newcode/round_robin_model.py +0 -0
  251. {newcode-0.2.5 → newcode-0.2.6}/newcode/session_storage.py +0 -0
  252. {newcode-0.2.5 → newcode-0.2.6}/newcode/status_display.py +0 -0
  253. {newcode-0.2.5 → newcode-0.2.6}/newcode/summarization_agent.py +0 -0
  254. {newcode-0.2.5 → newcode-0.2.6}/newcode/terminal_utils.py +0 -0
  255. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/agent_tools.py +0 -0
  256. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/ask_user_question/__init__.py +0 -0
  257. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/ask_user_question/constants.py +0 -0
  258. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/ask_user_question/demo_tui.py +0 -0
  259. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/ask_user_question/handler.py +0 -0
  260. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/ask_user_question/models.py +0 -0
  261. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/ask_user_question/registration.py +0 -0
  262. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/ask_user_question/renderers.py +0 -0
  263. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/ask_user_question/terminal_ui.py +0 -0
  264. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/ask_user_question/theme.py +0 -0
  265. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/ask_user_question/tui_loop.py +0 -0
  266. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/__init__.py +0 -0
  267. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/browser_control.py +0 -0
  268. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/browser_interactions.py +0 -0
  269. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/browser_locators.py +0 -0
  270. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/browser_manager.py +0 -0
  271. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/browser_navigation.py +0 -0
  272. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/browser_scripts.py +0 -0
  273. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/browser_workflows.py +0 -0
  274. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/chromium_terminal_manager.py +0 -0
  275. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/terminal_command_tools.py +0 -0
  276. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/browser/terminal_tools.py +0 -0
  277. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/command_runner.py +0 -0
  278. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/common.py +0 -0
  279. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/display.py +0 -0
  280. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/file_modifications.py +0 -0
  281. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/file_operations.py +0 -0
  282. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/skills_tools.py +0 -0
  283. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/subagent_context.py +0 -0
  284. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/tools_content.py +0 -0
  285. {newcode-0.2.5 → newcode-0.2.6}/newcode/tools/universal_constructor.py +0 -0
  286. {newcode-0.2.5 → newcode-0.2.6}/newcode/uvx_detection.py +0 -0
  287. {newcode-0.2.5 → newcode-0.2.6}/newcode/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: newcode
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: AI-powered code generation agent platform
5
5
  Project-URL: repository, https://github.com/janfeddersen-wq/new_code
6
6
  Project-URL: HomePage, https://github.com/janfeddersen-wq/new_code
@@ -57,7 +57,7 @@ NewCode is designed for practical software workflows in the terminal: inspect fi
57
57
  - Built-in agent system with a default coding agent and optional specialist workflows
58
58
  - MCP (Model Context Protocol) integration and server management
59
59
  - Interactive command-driven UX for model, agent, and settings management
60
- - Session autosave/restore and scheduler utilities
60
+ - Session autosave/restore utilities
61
61
  - Plugin/callback extensibility hooks
62
62
 
63
63
  ## Quick start
@@ -94,7 +94,6 @@ On first run, NewCode starts onboarding to help configure API keys and defaults.
94
94
  /config # Show current configuration
95
95
  /model # Select or switch model
96
96
  /agent # Select or switch agent
97
- /scheduler # Manage scheduled tasks
98
97
  /colors # Customize terminal UI colors
99
98
  /api # Manage built-in API server (start|stop|status)
100
99
  ```
@@ -15,7 +15,7 @@ NewCode is designed for practical software workflows in the terminal: inspect fi
15
15
  - Built-in agent system with a default coding agent and optional specialist workflows
16
16
  - MCP (Model Context Protocol) integration and server management
17
17
  - Interactive command-driven UX for model, agent, and settings management
18
- - Session autosave/restore and scheduler utilities
18
+ - Session autosave/restore utilities
19
19
  - Plugin/callback extensibility hooks
20
20
 
21
21
  ## Quick start
@@ -52,7 +52,6 @@ On first run, NewCode starts onboarding to help configure API keys and defaults.
52
52
  /config # Show current configuration
53
53
  /model # Select or switch model
54
54
  /agent # Select or switch agent
55
- /scheduler # Manage scheduled tasks
56
55
  /colors # Customize terminal UI colors
57
56
  /api # Manage built-in API server (start|stop|status)
58
57
  ```
@@ -71,6 +71,7 @@ from newcode.config import (
71
71
  get_value,
72
72
  )
73
73
  from newcode.error_logging import log_error
74
+ from newcode.image_utils import constrain_image_dimensions
74
75
  from newcode.keymap import cancel_agent_uses_signal, get_cancel_agent_char_code
75
76
  from newcode.mcp_ import get_mcp_manager
76
77
  from newcode.messaging import (
@@ -95,6 +96,20 @@ _delayed_compaction_requested = False
95
96
  _reload_count = 0
96
97
 
97
98
 
99
+ def _is_cloudflare_auth_error(exc: Exception) -> bool:
100
+ """Detect Cloudflare 400 auth failures from exception text."""
101
+ message = str(exc)
102
+ if not message:
103
+ return False
104
+
105
+ message_lower = message.lower()
106
+ return (
107
+ "cloudflare" in message_lower
108
+ and "400" in message_lower
109
+ and "bad request" in message_lower
110
+ )
111
+
112
+
98
113
  def _log_error_to_file(exc: Exception) -> Optional[str]:
99
114
  """Log detailed error information to ~/.newcode/error_logs/log_{timestamp}.txt.
100
115
 
@@ -1836,7 +1851,25 @@ class BaseAgent(ABC):
1836
1851
  # Build combined prompt payload when attachments are provided.
1837
1852
  attachment_parts: List[Any] = []
1838
1853
  if attachments:
1839
- attachment_parts.extend(list(attachments))
1854
+ # Constrain image dimensions for API compliance (Claude max 2000px for many-image requests)
1855
+ constrained = []
1856
+ for att in attachments:
1857
+ if (
1858
+ isinstance(att, BinaryContent)
1859
+ and hasattr(att, "media_type")
1860
+ and hasattr(att, "data")
1861
+ ):
1862
+ media_type = getattr(att, "media_type", "") or ""
1863
+ if media_type.startswith("image/"):
1864
+ new_data = constrain_image_dimensions(
1865
+ att.data, media_type=media_type
1866
+ )
1867
+ if new_data is not att.data:
1868
+ att = BinaryContent(data=new_data, media_type=media_type)
1869
+ constrained.append(att)
1870
+ else:
1871
+ constrained.append(att)
1872
+ attachment_parts.extend(constrained)
1840
1873
  if link_attachments:
1841
1874
  attachment_parts.extend(list(link_attachments))
1842
1875
 
@@ -1849,41 +1882,58 @@ class BaseAgent(ABC):
1849
1882
  prompt_payload = prompt
1850
1883
 
1851
1884
  async def run_agent_task():
1852
- try:
1853
- self.set_message_history(
1854
- self.prune_interrupted_tool_calls(self.get_message_history())
1855
- )
1885
+ _cloudflare_retry_attempted = False
1856
1886
 
1857
- # DELAYED COMPACTION: Check if we should attempt delayed compaction
1858
- if self.should_attempt_delayed_compaction():
1859
- emit_info(
1860
- "🔄 Attempting delayed compaction (tool calls completed)",
1861
- message_group="token_context_status",
1887
+ while True:
1888
+ _retry_after_cloudflare_refresh = False
1889
+ try:
1890
+ self.set_message_history(
1891
+ self.prune_interrupted_tool_calls(self.get_message_history())
1862
1892
  )
1863
- current_messages = self.get_message_history()
1864
- compacted_messages, _ = self.compact_messages(current_messages)
1865
- if compacted_messages != current_messages:
1866
- self.set_message_history(compacted_messages)
1893
+
1894
+ # DELAYED COMPACTION: Check if we should attempt delayed compaction
1895
+ if self.should_attempt_delayed_compaction():
1867
1896
  emit_info(
1868
- " Delayed compaction completed successfully",
1897
+ "🔄 Attempting delayed compaction (tool calls completed)",
1869
1898
  message_group="token_context_status",
1870
1899
  )
1900
+ current_messages = self.get_message_history()
1901
+ compacted_messages, _ = self.compact_messages(current_messages)
1902
+ if compacted_messages != current_messages:
1903
+ self.set_message_history(compacted_messages)
1904
+ emit_info(
1905
+ "✅ Delayed compaction completed successfully",
1906
+ message_group="token_context_status",
1907
+ )
1871
1908
 
1872
- usage_limits = UsageLimits(request_limit=get_message_limit())
1909
+ usage_limits = UsageLimits(request_limit=get_message_limit())
1873
1910
 
1874
- # Handle MCP servers - add them temporarily when using DBOS
1875
- if (
1876
- get_use_dbos()
1877
- and hasattr(self, "_mcp_servers")
1878
- and self._mcp_servers
1879
- ):
1880
- # Temporarily add MCP servers to the DBOS agent using internal _toolsets
1881
- original_toolsets = pydantic_agent._toolsets
1882
- pydantic_agent._toolsets = original_toolsets + self._mcp_servers
1883
- pydantic_agent._toolsets = original_toolsets + self._mcp_servers
1911
+ # Handle MCP servers - add them temporarily when using DBOS
1912
+ if (
1913
+ get_use_dbos()
1914
+ and hasattr(self, "_mcp_servers")
1915
+ and self._mcp_servers
1916
+ ):
1917
+ # Temporarily add MCP servers to the DBOS agent using internal _toolsets
1918
+ original_toolsets = pydantic_agent._toolsets
1919
+ pydantic_agent._toolsets = original_toolsets + self._mcp_servers
1920
+ pydantic_agent._toolsets = original_toolsets + self._mcp_servers
1884
1921
 
1885
- try:
1886
- # Set the workflow ID for DBOS context so DBOS and the agent ID match
1922
+ try:
1923
+ # Set the workflow ID for DBOS context so DBOS and the agent ID match
1924
+ with SetWorkflowID(group_id):
1925
+ result_ = await pydantic_agent.run(
1926
+ prompt_payload,
1927
+ message_history=self.get_message_history(),
1928
+ usage_limits=usage_limits,
1929
+ event_stream_handler=event_stream_handler,
1930
+ **kwargs,
1931
+ )
1932
+ return result_
1933
+ finally:
1934
+ # Always restore original toolsets
1935
+ pydantic_agent._toolsets = original_toolsets
1936
+ elif get_use_dbos():
1887
1937
  with SetWorkflowID(group_id):
1888
1938
  result_ = await pydantic_agent.run(
1889
1939
  prompt_payload,
@@ -1893,11 +1943,8 @@ class BaseAgent(ABC):
1893
1943
  **kwargs,
1894
1944
  )
1895
1945
  return result_
1896
- finally:
1897
- # Always restore original toolsets
1898
- pydantic_agent._toolsets = original_toolsets
1899
- elif get_use_dbos():
1900
- with SetWorkflowID(group_id):
1946
+ else:
1947
+ # Non-DBOS path (MCP servers are already included)
1901
1948
  result_ = await pydantic_agent.run(
1902
1949
  prompt_payload,
1903
1950
  message_history=self.get_message_history(),
@@ -1906,74 +1953,111 @@ class BaseAgent(ABC):
1906
1953
  **kwargs,
1907
1954
  )
1908
1955
  return result_
1909
- else:
1910
- # Non-DBOS path (MCP servers are already included)
1911
- result_ = await pydantic_agent.run(
1912
- prompt_payload,
1913
- message_history=self.get_message_history(),
1914
- usage_limits=usage_limits,
1915
- event_stream_handler=event_stream_handler,
1916
- **kwargs,
1956
+ except* UsageLimitExceeded as ule:
1957
+ emit_info(f"Usage limit exceeded: {str(ule)}", group_id=group_id)
1958
+ emit_info(
1959
+ "The agent has reached its usage limit. You can ask it to continue by saying 'please continue' or similar.",
1960
+ group_id=group_id,
1917
1961
  )
1918
- return result_
1919
- except* UsageLimitExceeded as ule:
1920
- emit_info(f"Usage limit exceeded: {str(ule)}", group_id=group_id)
1921
- emit_info(
1922
- "The agent has reached its usage limit. You can ask it to continue by saying 'please continue' or similar.",
1923
- group_id=group_id,
1924
- )
1925
- except* mcp.shared.exceptions.McpError as mcp_error:
1926
- emit_info(f"MCP server error: {str(mcp_error)}", group_id=group_id)
1927
- emit_info(f"{str(mcp_error)}", group_id=group_id)
1928
- emit_info(
1929
- "Try disabling any malfunctioning MCP servers", group_id=group_id
1930
- )
1931
- except* asyncio.exceptions.CancelledError:
1932
- emit_info("Cancelled")
1933
- if get_use_dbos():
1934
- await DBOS.cancel_workflow_async(group_id)
1935
- except* InterruptedError as ie:
1936
- emit_info(f"Interrupted: {str(ie)}")
1937
- if get_use_dbos():
1938
- await DBOS.cancel_workflow_async(group_id)
1939
- except* Exception as other_error:
1940
- # Filter out CancelledError and UsageLimitExceeded from the exception group - let it propagate
1941
- remaining_exceptions = []
1942
-
1943
- def collect_non_cancelled_exceptions(exc):
1944
- if isinstance(exc, ExceptionGroup):
1945
- for sub_exc in exc.exceptions:
1946
- collect_non_cancelled_exceptions(sub_exc)
1947
- elif not isinstance(
1948
- exc, (asyncio.CancelledError, UsageLimitExceeded)
1962
+ except* mcp.shared.exceptions.McpError as mcp_error:
1963
+ emit_info(f"MCP server error: {str(mcp_error)}", group_id=group_id)
1964
+ emit_info(f"{str(mcp_error)}", group_id=group_id)
1965
+ emit_info(
1966
+ "Try disabling any malfunctioning MCP servers",
1967
+ group_id=group_id,
1968
+ )
1969
+ except* asyncio.exceptions.CancelledError:
1970
+ emit_info("Cancelled")
1971
+ if get_use_dbos():
1972
+ await DBOS.cancel_workflow_async(group_id)
1973
+ except* InterruptedError as ie:
1974
+ emit_info(f"Interrupted: {str(ie)}")
1975
+ if get_use_dbos():
1976
+ await DBOS.cancel_workflow_async(group_id)
1977
+ except* Exception as other_error:
1978
+
1979
+ def contains_cloudflare_auth_error(exc: Exception) -> bool:
1980
+ if isinstance(exc, ExceptionGroup):
1981
+ return any(
1982
+ contains_cloudflare_auth_error(sub_exc)
1983
+ for sub_exc in exc.exceptions
1984
+ )
1985
+ return _is_cloudflare_auth_error(exc)
1986
+
1987
+ if (
1988
+ not _cloudflare_retry_attempted
1989
+ and contains_cloudflare_auth_error(other_error)
1949
1990
  ):
1950
- remaining_exceptions.append(exc)
1951
- emit_info(f"Unexpected error: {str(exc)}", group_id=group_id)
1952
- emit_info(f"{str(exc.args)}", group_id=group_id)
1953
- # Log to file for debugging
1954
- log_error(
1955
- exc,
1956
- context=f"Agent run (group_id={group_id})",
1957
- include_traceback=True,
1991
+ _cloudflare_retry_attempted = True
1992
+ emit_info(
1993
+ "Detected Cloudflare 400 error (likely expired token), attempting refresh...",
1994
+ group_id=group_id,
1958
1995
  )
1996
+ refreshed_token = None
1997
+ try:
1998
+ from newcode.plugins.claude_code_oauth.utils import (
1999
+ refresh_access_token,
2000
+ )
2001
+
2002
+ refreshed_token = refresh_access_token(force=True)
2003
+ except Exception as refresh_error:
2004
+ emit_info(
2005
+ f"Token refresh failed: {str(refresh_error)}",
2006
+ group_id=group_id,
2007
+ )
2008
+
2009
+ if refreshed_token:
2010
+ emit_info(
2011
+ "Token refresh successful, retrying request...",
2012
+ group_id=group_id,
2013
+ )
2014
+ _retry_after_cloudflare_refresh = True
2015
+
2016
+ if not _retry_after_cloudflare_refresh:
2017
+ # Filter out CancelledError and UsageLimitExceeded from the exception group - let it propagate
2018
+ remaining_exceptions = []
2019
+
2020
+ def collect_non_cancelled_exceptions(exc):
2021
+ if isinstance(exc, ExceptionGroup):
2022
+ for sub_exc in exc.exceptions:
2023
+ collect_non_cancelled_exceptions(sub_exc)
2024
+ elif not isinstance(
2025
+ exc, (asyncio.CancelledError, UsageLimitExceeded)
2026
+ ):
2027
+ remaining_exceptions.append(exc)
2028
+ emit_info(
2029
+ f"Unexpected error: {str(exc)}", group_id=group_id
2030
+ )
2031
+ emit_info(f"{str(exc.args)}", group_id=group_id)
2032
+ # Log to file for debugging
2033
+ log_error(
2034
+ exc,
2035
+ context=f"Agent run (group_id={group_id})",
2036
+ include_traceback=True,
2037
+ )
1959
2038
 
1960
- collect_non_cancelled_exceptions(other_error)
2039
+ collect_non_cancelled_exceptions(other_error)
1961
2040
 
1962
- # If there are CancelledError exceptions in the group, re-raise them
1963
- cancelled_exceptions = []
2041
+ # If there are CancelledError exceptions in the group, re-raise them
2042
+ cancelled_exceptions = []
1964
2043
 
1965
- def collect_cancelled_exceptions(exc):
1966
- if isinstance(exc, ExceptionGroup):
1967
- for sub_exc in exc.exceptions:
1968
- collect_cancelled_exceptions(sub_exc)
1969
- elif isinstance(exc, asyncio.CancelledError):
1970
- cancelled_exceptions.append(exc)
2044
+ def collect_cancelled_exceptions(exc):
2045
+ if isinstance(exc, ExceptionGroup):
2046
+ for sub_exc in exc.exceptions:
2047
+ collect_cancelled_exceptions(sub_exc)
2048
+ elif isinstance(exc, asyncio.CancelledError):
2049
+ cancelled_exceptions.append(exc)
1971
2050
 
1972
- collect_cancelled_exceptions(other_error)
1973
- finally:
1974
- self.set_message_history(
1975
- self.prune_interrupted_tool_calls(self.get_message_history())
1976
- )
2051
+ collect_cancelled_exceptions(other_error)
2052
+ finally:
2053
+ self.set_message_history(
2054
+ self.prune_interrupted_tool_calls(self.get_message_history())
2055
+ )
2056
+
2057
+ if _retry_after_cloudflare_refresh:
2058
+ continue
2059
+
2060
+ break
1977
2061
 
1978
2062
  # Create the task FIRST
1979
2063
  agent_task = asyncio.create_task(run_agent_task())
@@ -11,6 +11,8 @@ from typing import Iterable, List, Sequence
11
11
 
12
12
  from pydantic_ai import BinaryContent, DocumentUrl, ImageUrl
13
13
 
14
+ from newcode.image_utils import constrain_image_dimensions
15
+
14
16
  SUPPORTED_INLINE_SCHEMES = {"http", "https"}
15
17
 
16
18
  # Maximum path length to consider - conservative limit to avoid OS errors
@@ -341,6 +343,8 @@ def parse_prompt_attachments(prompt: str) -> ProcessedPrompt:
341
343
  except AttachmentParsingError:
342
344
  # Silently ignore unreadable attachments to reduce prompt noise
343
345
  continue
346
+ # Constrain image dimensions for API compliance (Claude max 2000px for many-image requests)
347
+ data = constrain_image_dimensions(data, media_type=media_type)
344
348
  attachments.append(
345
349
  PromptAttachment(
346
350
  placeholder=detection.placeholder,
@@ -16,6 +16,8 @@ import threading
16
16
  import time
17
17
  from typing import Optional
18
18
 
19
+ from newcode.image_utils import constrain_image_dimensions
20
+
19
21
  # Try to import PIL - it's optional but needed for clipboard image support
20
22
  try:
21
23
  from PIL import Image, ImageGrab
@@ -42,7 +44,7 @@ logger = logging.getLogger(__name__)
42
44
 
43
45
  # Constants
44
46
  MAX_IMAGE_SIZE_BYTES = 10 * 1024 * 1024 # 10MB
45
- MAX_IMAGE_DIMENSION = 4096 # Max width/height for resize
47
+ MAX_IMAGE_DIMENSION = 1920 # Max width/height for resize
46
48
  MAX_PENDING_IMAGES = (
47
49
  10 # SEC-CLIP-001: Limit pending images to prevent memory exhaustion
48
50
  )
@@ -317,6 +319,7 @@ def get_clipboard_image() -> Optional[bytes]:
317
319
  )
318
320
  return None
319
321
 
322
+ image_bytes = constrain_image_dimensions(image_bytes)
320
323
  return image_bytes
321
324
 
322
325
  # Windows/macOS path - use PIL
@@ -356,6 +359,7 @@ def get_clipboard_image() -> Optional[bytes]:
356
359
  image_bytes = buffer.getvalue()
357
360
 
358
361
  logger.info(f"Clipboard image size: {len(image_bytes) / 1024:.1f}KB")
362
+ image_bytes = constrain_image_dimensions(image_bytes)
359
363
  return image_bytes
360
364
 
361
365
  except Exception as e:
@@ -0,0 +1,85 @@
1
+ """Image processing utilities for API compliance.
2
+
3
+ Ensures images sent to LLM APIs respect dimension limits.
4
+ The Claude API enforces a 2000px max dimension for many-image requests.
5
+ We use 1920px as default to provide a safety margin.
6
+ """
7
+
8
+ import io
9
+ import logging
10
+ from typing import Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Claude API max dimension for many-image requests is 2000px.
15
+ # Use 1920 for safety margin.
16
+ MAX_IMAGE_DIMENSION = 1920
17
+
18
+ try:
19
+ from PIL import Image
20
+
21
+ PIL_AVAILABLE = True
22
+ except ImportError:
23
+ PIL_AVAILABLE = False
24
+ Image = None # type: ignore[misc, assignment]
25
+
26
+
27
+ def constrain_image_dimensions(
28
+ image_bytes: bytes,
29
+ max_dim: int = MAX_IMAGE_DIMENSION,
30
+ media_type: Optional[str] = None,
31
+ ) -> bytes:
32
+ """Resize image if any dimension exceeds max_dim, preserving aspect ratio.
33
+
34
+ Args:
35
+ image_bytes: Raw image bytes (PNG, JPEG, etc.).
36
+ max_dim: Maximum allowed width or height in pixels.
37
+ media_type: Optional MIME type hint. If provided and not an image
38
+ type, returns bytes unchanged.
39
+
40
+ Returns:
41
+ Original bytes if within limits or PIL unavailable, resized PNG bytes otherwise.
42
+ """
43
+ # Skip non-image content
44
+ if media_type and not media_type.startswith("image/"):
45
+ return image_bytes
46
+
47
+ if not PIL_AVAILABLE or Image is None:
48
+ logger.debug("PIL not available, skipping image dimension check")
49
+ return image_bytes
50
+
51
+ if not image_bytes:
52
+ return image_bytes
53
+
54
+ try:
55
+ img = Image.open(io.BytesIO(image_bytes))
56
+ width, height = img.size
57
+
58
+ if width <= max_dim and height <= max_dim:
59
+ return image_bytes
60
+
61
+ # Calculate scale factor based on the larger dimension
62
+ scale = max_dim / max(width, height)
63
+ new_width = int(width * scale)
64
+ new_height = int(height * scale)
65
+
66
+ # Ensure minimum dimensions
67
+ new_width = max(new_width, 1)
68
+ new_height = max(new_height, 1)
69
+
70
+ resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
71
+
72
+ # Save as PNG to preserve quality
73
+ output = io.BytesIO()
74
+ resized.save(output, format="PNG", optimize=True)
75
+ output.seek(0)
76
+
77
+ logger.info(
78
+ f"Constrained image dimensions from {width}x{height} "
79
+ f"to {new_width}x{new_height} (max_dim={max_dim})"
80
+ )
81
+ return output.read()
82
+
83
+ except Exception as e:
84
+ logger.warning(f"Failed to constrain image dimensions: {e}")
85
+ return image_bytes
@@ -380,6 +380,55 @@ def _register_model_types() -> List[Dict[str, Any]]:
380
380
  # Using a dict to allow multiple concurrent agent runs (keyed by session_id)
381
381
  _active_heartbeats: Dict[str, Any] = {}
382
382
 
383
+ # Persistent session-level heartbeat that runs for the entire app lifetime
384
+ _session_heartbeat: Optional[Any] = None
385
+
386
+
387
+ async def _on_startup() -> None:
388
+ """Start a persistent token refresh heartbeat for idle sessions."""
389
+ global _session_heartbeat
390
+
391
+ try:
392
+ if _session_heartbeat is not None:
393
+ return
394
+
395
+ tokens = load_stored_tokens()
396
+ if not tokens:
397
+ return
398
+
399
+ from .token_refresh_heartbeat import (
400
+ SESSION_HEARTBEAT_INTERVAL_SECONDS,
401
+ TokenRefreshHeartbeat,
402
+ )
403
+
404
+ heartbeat = TokenRefreshHeartbeat(interval=SESSION_HEARTBEAT_INTERVAL_SECONDS)
405
+ await heartbeat.start()
406
+ _session_heartbeat = heartbeat
407
+ logger.debug(
408
+ "Started persistent session-level token refresh heartbeat (interval: 1h)"
409
+ )
410
+ except Exception as exc:
411
+ logger.debug(
412
+ "Failed to start persistent session-level token refresh heartbeat: %s", exc
413
+ )
414
+
415
+
416
+ async def _on_shutdown() -> None:
417
+ """Stop the persistent session-level token refresh heartbeat."""
418
+ global _session_heartbeat
419
+
420
+ try:
421
+ if _session_heartbeat is None:
422
+ return
423
+
424
+ await _session_heartbeat.stop()
425
+ _session_heartbeat = None
426
+ logger.debug("Stopped persistent session-level token refresh heartbeat")
427
+ except Exception as exc:
428
+ logger.debug(
429
+ "Failed to stop persistent session-level token refresh heartbeat: %s", exc
430
+ )
431
+
383
432
 
384
433
  async def _on_agent_run_start(
385
434
  agent_name: str,
@@ -449,5 +498,7 @@ async def _on_agent_run_end(
449
498
  register_callback("custom_command_help", _custom_help)
450
499
  register_callback("custom_command", _handle_custom_command)
451
500
  register_callback("register_model_type", _register_model_types)
501
+ register_callback("startup", _on_startup)
502
+ register_callback("shutdown", _on_shutdown)
452
503
  register_callback("agent_run_start", _on_agent_run_start)
453
504
  register_callback("agent_run_end", _on_agent_run_end)
@@ -28,6 +28,10 @@ HEARTBEAT_INTERVAL_SECONDS = 120
28
28
  # Minimum time between refresh attempts to avoid hammering the endpoint
29
29
  MIN_REFRESH_INTERVAL_SECONDS = 60
30
30
 
31
+ # Session-level heartbeat interval - refresh token once per hour during idle sessions
32
+ # This prevents tokens from expiring when the app is open but no agent is running
33
+ SESSION_HEARTBEAT_INTERVAL_SECONDS = 3600
34
+
31
35
  # Global tracking of last refresh time to coordinate across heartbeats
32
36
  _last_refresh_time: float = 0.0
33
37
 
@@ -88,19 +88,6 @@ from newcode.tools.file_operations import (
88
88
  register_list_files,
89
89
  register_read_file,
90
90
  )
91
-
92
- # Scheduler tools
93
- from newcode.tools.scheduler_tools import (
94
- register_scheduler_create_task,
95
- register_scheduler_daemon_status,
96
- register_scheduler_delete_task,
97
- register_scheduler_list_tasks,
98
- register_scheduler_run_task,
99
- register_scheduler_start_daemon,
100
- register_scheduler_stop_daemon,
101
- register_scheduler_toggle_task,
102
- register_scheduler_view_log,
103
- )
104
91
  from newcode.tools.skills_tools import (
105
92
  register_activate_skill,
106
93
  register_list_or_search_skills,
@@ -188,16 +175,6 @@ TOOL_REGISTRY = {
188
175
  "list_or_search_skills": register_list_or_search_skills,
189
176
  # Universal Constructor
190
177
  "universal_constructor": register_universal_constructor,
191
- # Scheduler Tools
192
- "scheduler_list_tasks": register_scheduler_list_tasks,
193
- "scheduler_create_task": register_scheduler_create_task,
194
- "scheduler_delete_task": register_scheduler_delete_task,
195
- "scheduler_toggle_task": register_scheduler_toggle_task,
196
- "scheduler_daemon_status": register_scheduler_daemon_status,
197
- "scheduler_start_daemon": register_scheduler_start_daemon,
198
- "scheduler_stop_daemon": register_scheduler_stop_daemon,
199
- "scheduler_run_task": register_scheduler_run_task,
200
- "scheduler_view_log": register_scheduler_view_log,
201
178
  }
202
179
 
203
180
 
@@ -12,6 +12,7 @@ from typing import Any, Dict, Optional, Union
12
12
 
13
13
  from pydantic_ai import BinaryContent, RunContext, ToolReturn
14
14
 
15
+ from newcode.image_utils import constrain_image_dimensions
15
16
  from newcode.messaging import emit_error, emit_info, emit_success
16
17
  from newcode.tools.common import generate_group_id
17
18
 
@@ -122,13 +123,16 @@ async def take_screenshot(
122
123
 
123
124
  screenshot_path = result.get("screenshot_path", "(not saved)")
124
125
 
126
+ # Constrain dimensions for API compliance (Claude max 2000px for many-image requests)
127
+ constrained_bytes = constrain_image_dimensions(result["screenshot_bytes"])
128
+
125
129
  # Return as ToolReturn with BinaryContent so the model can SEE the image!
126
130
  return ToolReturn(
127
131
  return_value=f"Screenshot captured successfully. Saved to: {screenshot_path}",
128
132
  content=[
129
133
  f"Here's the browser screenshot ({target}):",
130
134
  BinaryContent(
131
- data=result["screenshot_bytes"],
135
+ data=constrained_bytes,
132
136
  media_type="image/png",
133
137
  ),
134
138
  "Please analyze what you see and describe any relevant details.",