newcode 0.2.4__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.4 → newcode-0.2.6}/PKG-INFO +2 -3
  2. {newcode-0.2.4 → newcode-0.2.6}/README.md +1 -2
  3. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/base_agent.py +178 -94
  4. {newcode-0.2.4 → newcode-0.2.6}/newcode/claude_cache_client.py +15 -61
  5. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/attachments.py +4 -0
  6. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/clipboard.py +5 -1
  7. newcode-0.2.6/newcode/image_utils.py +85 -0
  8. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/register_callbacks.py +51 -0
  9. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/token_refresh_heartbeat.py +4 -0
  10. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/__init__.py +0 -23
  11. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/browser_screenshot.py +5 -1
  12. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/terminal_screenshot_tools.py +6 -2
  13. {newcode-0.2.4 → newcode-0.2.6}/pyproject.toml +1 -1
  14. newcode-0.2.4/newcode/agents/agent_scheduler.py +0 -120
  15. newcode-0.2.4/newcode/plugins/scheduler/__init__.py +0 -1
  16. newcode-0.2.4/newcode/plugins/scheduler/register_callbacks.py +0 -88
  17. newcode-0.2.4/newcode/plugins/scheduler/scheduler_menu.py +0 -520
  18. newcode-0.2.4/newcode/plugins/scheduler/scheduler_wizard.py +0 -341
  19. newcode-0.2.4/newcode/scheduler/__init__.py +0 -41
  20. newcode-0.2.4/newcode/scheduler/__main__.py +0 -9
  21. newcode-0.2.4/newcode/scheduler/cli.py +0 -118
  22. newcode-0.2.4/newcode/scheduler/config.py +0 -126
  23. newcode-0.2.4/newcode/scheduler/daemon.py +0 -280
  24. newcode-0.2.4/newcode/scheduler/executor.py +0 -155
  25. newcode-0.2.4/newcode/scheduler/platform.py +0 -19
  26. newcode-0.2.4/newcode/scheduler/platform_unix.py +0 -22
  27. newcode-0.2.4/newcode/scheduler/platform_win.py +0 -32
  28. newcode-0.2.4/newcode/tools/scheduler_tools.py +0 -412
  29. {newcode-0.2.4 → newcode-0.2.6}/.gitignore +0 -0
  30. {newcode-0.2.4 → newcode-0.2.6}/LICENSE +0 -0
  31. {newcode-0.2.4 → newcode-0.2.6}/newcode/__init__.py +0 -0
  32. {newcode-0.2.4 → newcode-0.2.6}/newcode/__main__.py +0 -0
  33. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/__init__.py +0 -0
  34. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_c_reviewer.py +0 -0
  35. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_code_agent.py +0 -0
  36. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_code_reviewer.py +0 -0
  37. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_cpp_reviewer.py +0 -0
  38. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_creator_agent.py +0 -0
  39. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_golang_reviewer.py +0 -0
  40. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_javascript_reviewer.py +0 -0
  41. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_manager.py +0 -0
  42. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_pack_leader.py +0 -0
  43. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_planning.py +0 -0
  44. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_python_programmer.py +0 -0
  45. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_python_reviewer.py +0 -0
  46. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_qa_browser.py +0 -0
  47. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_qa_expert.py +0 -0
  48. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_security_auditor.py +0 -0
  49. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_terminal_qa.py +0 -0
  50. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/agent_typescript_reviewer.py +0 -0
  51. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/event_stream_handler.py +0 -0
  52. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/json_agent.py +0 -0
  53. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/pack/__init__.py +0 -0
  54. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/pack/bloodhound.py +0 -0
  55. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/pack/husky.py +0 -0
  56. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/pack/retriever.py +0 -0
  57. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/pack/shepherd.py +0 -0
  58. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/pack/terrier.py +0 -0
  59. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/pack/watchdog.py +0 -0
  60. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/prompt_reviewer.py +0 -0
  61. {newcode-0.2.4 → newcode-0.2.6}/newcode/agents/subagent_stream_handler.py +0 -0
  62. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/__init__.py +0 -0
  63. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/app.py +0 -0
  64. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/main.py +0 -0
  65. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/pty_manager.py +0 -0
  66. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/routers/__init__.py +0 -0
  67. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/routers/agents.py +0 -0
  68. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/routers/commands.py +0 -0
  69. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/routers/config.py +0 -0
  70. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/routers/sessions.py +0 -0
  71. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/templates/terminal.html +0 -0
  72. {newcode-0.2.4 → newcode-0.2.6}/newcode/api/websocket.py +0 -0
  73. {newcode-0.2.4 → newcode-0.2.6}/newcode/callbacks.py +0 -0
  74. {newcode-0.2.4 → newcode-0.2.6}/newcode/chatgpt_codex_client.py +0 -0
  75. {newcode-0.2.4 → newcode-0.2.6}/newcode/cli_runner.py +0 -0
  76. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/__init__.py +0 -0
  77. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/add_model_menu.py +0 -0
  78. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/agent_menu.py +0 -0
  79. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/autosave_menu.py +0 -0
  80. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/colors_menu.py +0 -0
  81. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/command_handler.py +0 -0
  82. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/command_registry.py +0 -0
  83. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/config_commands.py +0 -0
  84. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/core_commands.py +0 -0
  85. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/diff_menu.py +0 -0
  86. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/file_path_completion.py +0 -0
  87. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/load_context_completion.py +0 -0
  88. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/__init__.py +0 -0
  89. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/base.py +0 -0
  90. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/catalog_server_installer.py +0 -0
  91. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/custom_server_form.py +0 -0
  92. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/custom_server_installer.py +0 -0
  93. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/edit_command.py +0 -0
  94. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/handler.py +0 -0
  95. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/help_command.py +0 -0
  96. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/install_command.py +0 -0
  97. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/install_menu.py +0 -0
  98. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/list_command.py +0 -0
  99. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/logs_command.py +0 -0
  100. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/remove_command.py +0 -0
  101. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/restart_command.py +0 -0
  102. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/search_command.py +0 -0
  103. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/start_all_command.py +0 -0
  104. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/start_command.py +0 -0
  105. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/status_command.py +0 -0
  106. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/stop_all_command.py +0 -0
  107. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/stop_command.py +0 -0
  108. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/test_command.py +0 -0
  109. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/utils.py +0 -0
  110. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp/wizard_utils.py +0 -0
  111. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/mcp_completion.py +0 -0
  112. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/model_picker_completion.py +0 -0
  113. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/model_settings_menu.py +0 -0
  114. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/motd.py +0 -0
  115. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/onboarding_slides.py +0 -0
  116. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/onboarding_wizard.py +0 -0
  117. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/pin_command_completion.py +0 -0
  118. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/prompt_toolkit_completion.py +0 -0
  119. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/session_commands.py +0 -0
  120. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/skills_completion.py +0 -0
  121. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/uc_menu.py +0 -0
  122. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/utils.py +0 -0
  123. {newcode-0.2.4 → newcode-0.2.6}/newcode/command_line/wiggum_state.py +0 -0
  124. {newcode-0.2.4 → newcode-0.2.6}/newcode/config.py +0 -0
  125. {newcode-0.2.4 → newcode-0.2.6}/newcode/error_logging.py +0 -0
  126. {newcode-0.2.4 → newcode-0.2.6}/newcode/gemini_code_assist.py +0 -0
  127. {newcode-0.2.4 → newcode-0.2.6}/newcode/gemini_model.py +0 -0
  128. {newcode-0.2.4 → newcode-0.2.6}/newcode/hook_engine/README.md +0 -0
  129. {newcode-0.2.4 → newcode-0.2.6}/newcode/hook_engine/__init__.py +0 -0
  130. {newcode-0.2.4 → newcode-0.2.6}/newcode/hook_engine/aliases.py +0 -0
  131. {newcode-0.2.4 → newcode-0.2.6}/newcode/hook_engine/engine.py +0 -0
  132. {newcode-0.2.4 → newcode-0.2.6}/newcode/hook_engine/executor.py +0 -0
  133. {newcode-0.2.4 → newcode-0.2.6}/newcode/hook_engine/matcher.py +0 -0
  134. {newcode-0.2.4 → newcode-0.2.6}/newcode/hook_engine/models.py +0 -0
  135. {newcode-0.2.4 → newcode-0.2.6}/newcode/hook_engine/registry.py +0 -0
  136. {newcode-0.2.4 → newcode-0.2.6}/newcode/hook_engine/validator.py +0 -0
  137. {newcode-0.2.4 → newcode-0.2.6}/newcode/http_utils.py +0 -0
  138. {newcode-0.2.4 → newcode-0.2.6}/newcode/keymap.py +0 -0
  139. {newcode-0.2.4 → newcode-0.2.6}/newcode/main.py +0 -0
  140. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/__init__.py +0 -0
  141. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/async_lifecycle.py +0 -0
  142. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/blocking_startup.py +0 -0
  143. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/captured_stdio_server.py +0 -0
  144. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/circuit_breaker.py +0 -0
  145. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/config_wizard.py +0 -0
  146. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/dashboard.py +0 -0
  147. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/error_isolation.py +0 -0
  148. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/examples/retry_example.py +0 -0
  149. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/health_monitor.py +0 -0
  150. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/managed_server.py +0 -0
  151. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/manager.py +0 -0
  152. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/mcp_logs.py +0 -0
  153. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/registry.py +0 -0
  154. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/retry_manager.py +0 -0
  155. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/server_registry_catalog.py +0 -0
  156. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/status_tracker.py +0 -0
  157. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_/system_tools.py +0 -0
  158. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_prompts/__init__.py +0 -0
  159. {newcode-0.2.4 → newcode-0.2.6}/newcode/mcp_prompts/hook_creator.py +0 -0
  160. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/__init__.py +0 -0
  161. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/bus.py +0 -0
  162. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/commands.py +0 -0
  163. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/markdown_patches.py +0 -0
  164. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/message_queue.py +0 -0
  165. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/messages.py +0 -0
  166. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/queue_console.py +0 -0
  167. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/renderers.py +0 -0
  168. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/rich_renderer.py +0 -0
  169. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/spinner/__init__.py +0 -0
  170. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/spinner/console_spinner.py +0 -0
  171. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/spinner/spinner_base.py +0 -0
  172. {newcode-0.2.4 → newcode-0.2.6}/newcode/messaging/subagent_console.py +0 -0
  173. {newcode-0.2.4 → newcode-0.2.6}/newcode/model_factory.py +0 -0
  174. {newcode-0.2.4 → newcode-0.2.6}/newcode/model_switching.py +0 -0
  175. {newcode-0.2.4 → newcode-0.2.6}/newcode/model_utils.py +0 -0
  176. {newcode-0.2.4 → newcode-0.2.6}/newcode/models.json +0 -0
  177. {newcode-0.2.4 → newcode-0.2.6}/newcode/models_dev_api.json +0 -0
  178. {newcode-0.2.4 → newcode-0.2.6}/newcode/models_dev_parser.py +0 -0
  179. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/__init__.py +0 -0
  180. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/__init__.py +0 -0
  181. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/config.py +0 -0
  182. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/discovery.py +0 -0
  183. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/downloader.py +0 -0
  184. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/installer.py +0 -0
  185. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/metadata.py +0 -0
  186. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/prompt_builder.py +0 -0
  187. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/register_callbacks.py +0 -0
  188. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/remote_catalog.py +0 -0
  189. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/skill_catalog.py +0 -0
  190. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/skills_install_menu.py +0 -0
  191. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/agent_skills/skills_menu.py +0 -0
  192. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/__init__.py +0 -0
  193. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/accounts.py +0 -0
  194. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/antigravity_model.py +0 -0
  195. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/config.py +0 -0
  196. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/constants.py +0 -0
  197. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/oauth.py +0 -0
  198. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/register_callbacks.py +0 -0
  199. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/storage.py +0 -0
  200. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/test_plugin.py +0 -0
  201. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/token.py +0 -0
  202. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/transport.py +0 -0
  203. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/antigravity_oauth/utils.py +0 -0
  204. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/__init__.py +0 -0
  205. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/config.py +0 -0
  206. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  207. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  208. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/test_plugin.py +0 -0
  209. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/chatgpt_oauth/utils.py +0 -0
  210. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_hooks/__init__.py +0 -0
  211. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_hooks/config.py +0 -0
  212. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_hooks/register_callbacks.py +0 -0
  213. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/README.md +0 -0
  214. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/SETUP.md +0 -0
  215. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/__init__.py +0 -0
  216. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/config.py +0 -0
  217. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/test_plugin.py +0 -0
  218. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/claude_code_oauth/utils.py +0 -0
  219. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/customizable_commands/__init__.py +0 -0
  220. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/customizable_commands/register_callbacks.py +0 -0
  221. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/example_custom_command/README.md +0 -0
  222. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/example_custom_command/register_callbacks.py +0 -0
  223. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/file_permission_handler/__init__.py +0 -0
  224. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/file_permission_handler/register_callbacks.py +0 -0
  225. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/frontend_emitter/__init__.py +0 -0
  226. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/frontend_emitter/emitter.py +0 -0
  227. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/frontend_emitter/register_callbacks.py +0 -0
  228. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/hook_creator/__init__.py +0 -0
  229. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/hook_creator/register_callbacks.py +0 -0
  230. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/hook_manager/__init__.py +0 -0
  231. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/hook_manager/config.py +0 -0
  232. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/hook_manager/hooks_menu.py +0 -0
  233. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/hook_manager/register_callbacks.py +0 -0
  234. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/oauth_puppy_html.py +0 -0
  235. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/shell_safety/__init__.py +0 -0
  236. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/shell_safety/agent_shell_safety.py +0 -0
  237. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/shell_safety/command_cache.py +0 -0
  238. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/shell_safety/register_callbacks.py +0 -0
  239. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/synthetic_status/__init__.py +0 -0
  240. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/synthetic_status/register_callbacks.py +0 -0
  241. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/synthetic_status/status_api.py +0 -0
  242. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/universal_constructor/__init__.py +0 -0
  243. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/universal_constructor/models.py +0 -0
  244. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/universal_constructor/register_callbacks.py +0 -0
  245. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/universal_constructor/registry.py +0 -0
  246. {newcode-0.2.4 → newcode-0.2.6}/newcode/plugins/universal_constructor/sandbox.py +0 -0
  247. {newcode-0.2.4 → newcode-0.2.6}/newcode/prompts/antigravity_system_prompt.md +0 -0
  248. {newcode-0.2.4 → newcode-0.2.6}/newcode/pydantic_patches.py +0 -0
  249. {newcode-0.2.4 → newcode-0.2.6}/newcode/reopenable_async_client.py +0 -0
  250. {newcode-0.2.4 → newcode-0.2.6}/newcode/round_robin_model.py +0 -0
  251. {newcode-0.2.4 → newcode-0.2.6}/newcode/session_storage.py +0 -0
  252. {newcode-0.2.4 → newcode-0.2.6}/newcode/status_display.py +0 -0
  253. {newcode-0.2.4 → newcode-0.2.6}/newcode/summarization_agent.py +0 -0
  254. {newcode-0.2.4 → newcode-0.2.6}/newcode/terminal_utils.py +0 -0
  255. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/agent_tools.py +0 -0
  256. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/ask_user_question/__init__.py +0 -0
  257. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/ask_user_question/constants.py +0 -0
  258. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/ask_user_question/demo_tui.py +0 -0
  259. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/ask_user_question/handler.py +0 -0
  260. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/ask_user_question/models.py +0 -0
  261. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/ask_user_question/registration.py +0 -0
  262. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/ask_user_question/renderers.py +0 -0
  263. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/ask_user_question/terminal_ui.py +0 -0
  264. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/ask_user_question/theme.py +0 -0
  265. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/ask_user_question/tui_loop.py +0 -0
  266. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/__init__.py +0 -0
  267. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/browser_control.py +0 -0
  268. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/browser_interactions.py +0 -0
  269. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/browser_locators.py +0 -0
  270. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/browser_manager.py +0 -0
  271. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/browser_navigation.py +0 -0
  272. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/browser_scripts.py +0 -0
  273. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/browser_workflows.py +0 -0
  274. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/chromium_terminal_manager.py +0 -0
  275. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/terminal_command_tools.py +0 -0
  276. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/browser/terminal_tools.py +0 -0
  277. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/command_runner.py +0 -0
  278. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/common.py +0 -0
  279. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/display.py +0 -0
  280. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/file_modifications.py +0 -0
  281. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/file_operations.py +0 -0
  282. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/skills_tools.py +0 -0
  283. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/subagent_context.py +0 -0
  284. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/tools_content.py +0 -0
  285. {newcode-0.2.4 → newcode-0.2.6}/newcode/tools/universal_constructor.py +0 -0
  286. {newcode-0.2.4 → newcode-0.2.6}/newcode/uvx_detection.py +0 -0
  287. {newcode-0.2.4 → 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.4
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())
@@ -371,12 +371,10 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
371
371
  is_auth_error = response.status_code in (401, 403)
372
372
 
373
373
  if response.status_code == 400:
374
- is_auth_error = self._is_cloudflare_html_error(
375
- response
376
- ) or self._is_token_expired_error(response)
374
+ is_auth_error = self._is_cloudflare_html_error(response)
377
375
  if is_auth_error:
378
376
  logger.info(
379
- "Detected 400 auth/token error, attempting token refresh"
377
+ "Detected Cloudflare 400 error (expired token), attempting token refresh"
380
378
  )
381
379
 
382
380
  if is_auth_error:
@@ -537,79 +535,35 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
537
535
  """Check if this is a Cloudflare HTML error response.
538
536
 
539
537
  Cloudflare often returns HTML error pages with status 400 when
540
- there are authentication issues.
538
+ there are authentication issues. The response looks like:
539
+ <html>
540
+ <head><title>400 Bad Request</title></head>
541
+ <body>
542
+ <center><h1>400 Bad Request</h1></center>
543
+ <hr><center>cloudflare</center>
544
+ </body>
545
+ </html>
541
546
  """
542
- # Check content type
543
- content_type = response.headers.get("content-type", "")
544
- if "text/html" not in content_type.lower():
545
- return False
546
-
547
- # Check if body contains Cloudflare markers
548
547
  try:
549
- # Read response body if not already consumed
548
+ body = None
549
+ # Try reading the body from _content first (already consumed)
550
550
  if hasattr(response, "_content") and response._content:
551
551
  body = response._content.decode("utf-8", errors="ignore")
552
552
  else:
553
- # Try to read the text (this might be already consumed)
554
553
  try:
555
554
  body = response.text
556
555
  except Exception:
557
556
  return False
558
557
 
559
- # Look for Cloudflare and 400 Bad Request markers
558
+ if not body:
559
+ return False
560
+
560
561
  body_lower = body.lower()
561
562
  return "cloudflare" in body_lower and "400 bad request" in body_lower
562
563
  except Exception as exc:
563
564
  logger.debug("Error checking for Cloudflare error: %s", exc)
564
565
  return False
565
566
 
566
- @staticmethod
567
- def _is_token_expired_error(response: httpx.Response) -> bool:
568
- """Check if a 400 response indicates an expired or invalid OAuth token.
569
-
570
- The Anthropic API returns JSON 400 errors when the token is expired.
571
- Also checks stored token expiry as a heuristic.
572
- """
573
- try:
574
- # Check if the JSON body mentions auth/token issues
575
- body = None
576
- if hasattr(response, "_content") and response._content:
577
- body = response._content.decode("utf-8", errors="ignore")
578
- else:
579
- try:
580
- body = response.text
581
- except Exception:
582
- pass
583
-
584
- if body:
585
- body_lower = body.lower()
586
- auth_markers = (
587
- "expired",
588
- "invalid_token",
589
- "invalid_grant",
590
- "token",
591
- "unauthorized",
592
- "authentication",
593
- )
594
- if any(marker in body_lower for marker in auth_markers):
595
- return True
596
-
597
- # Fallback: check if our stored token is actually expired
598
- from newcode.plugins.claude_code_oauth.utils import (
599
- is_token_expired,
600
- load_stored_tokens,
601
- )
602
-
603
- tokens = load_stored_tokens()
604
- if tokens and is_token_expired(tokens):
605
- logger.info("Stored token is expired, treating 400 as auth error")
606
- return True
607
-
608
- except Exception as exc:
609
- logger.debug("Error checking for token expired error: %s", exc)
610
-
611
- return False
612
-
613
567
  def _refresh_claude_oauth_token(self) -> str | None:
614
568
  try:
615
569
  from newcode.plugins.claude_code_oauth.utils import refresh_access_token
@@ -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