code-puppy 0.0.340__tar.gz → 0.0.342__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 (182) hide show
  1. {code_puppy-0.0.340 → code_puppy-0.0.342}/PKG-INFO +1 -1
  2. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/base_agent.py +24 -1
  3. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/claude_cache_client.py +104 -0
  4. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/autosave_menu.py +18 -24
  5. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/prompt_toolkit_completion.py +24 -32
  6. {code_puppy-0.0.340 → code_puppy-0.0.342}/pyproject.toml +1 -1
  7. {code_puppy-0.0.340 → code_puppy-0.0.342}/.gitignore +0 -0
  8. {code_puppy-0.0.340 → code_puppy-0.0.342}/LICENSE +0 -0
  9. {code_puppy-0.0.340 → code_puppy-0.0.342}/README.md +0 -0
  10. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/__init__.py +0 -0
  11. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/__main__.py +0 -0
  12. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/__init__.py +0 -0
  13. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_c_reviewer.py +0 -0
  14. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_code_puppy.py +0 -0
  15. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_code_reviewer.py +0 -0
  16. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  17. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_creator_agent.py +0 -0
  18. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  19. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  20. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_manager.py +0 -0
  21. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_planning.py +0 -0
  22. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_python_programmer.py +0 -0
  23. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_python_reviewer.py +0 -0
  24. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_qa_expert.py +0 -0
  25. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_qa_kitten.py +0 -0
  26. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_security_auditor.py +0 -0
  27. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  28. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/json_agent.py +0 -0
  29. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/agents/prompt_reviewer.py +0 -0
  30. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/callbacks.py +0 -0
  31. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/chatgpt_codex_client.py +0 -0
  32. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/cli_runner.py +0 -0
  33. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/__init__.py +0 -0
  34. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/add_model_menu.py +0 -0
  35. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/attachments.py +0 -0
  36. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/clipboard.py +0 -0
  37. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/colors_menu.py +0 -0
  38. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/command_handler.py +0 -0
  39. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/command_registry.py +0 -0
  40. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/config_commands.py +0 -0
  41. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/core_commands.py +0 -0
  42. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/diff_menu.py +0 -0
  43. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/file_path_completion.py +0 -0
  44. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/load_context_completion.py +0 -0
  45. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/__init__.py +0 -0
  46. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/add_command.py +0 -0
  47. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/base.py +0 -0
  48. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  49. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  50. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  51. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/edit_command.py +0 -0
  52. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/handler.py +0 -0
  53. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/help_command.py +0 -0
  54. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/install_command.py +0 -0
  55. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/install_menu.py +0 -0
  56. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/list_command.py +0 -0
  57. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/logs_command.py +0 -0
  58. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/remove_command.py +0 -0
  59. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/restart_command.py +0 -0
  60. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/search_command.py +0 -0
  61. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  62. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/start_command.py +0 -0
  63. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/status_command.py +0 -0
  64. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  65. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/stop_command.py +0 -0
  66. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/test_command.py +0 -0
  67. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/utils.py +0 -0
  68. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  69. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/mcp_completion.py +0 -0
  70. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/model_picker_completion.py +0 -0
  71. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/model_settings_menu.py +0 -0
  72. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/motd.py +0 -0
  73. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/onboarding_slides.py +0 -0
  74. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/onboarding_wizard.py +0 -0
  75. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/pin_command_completion.py +0 -0
  76. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/session_commands.py +0 -0
  77. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/command_line/utils.py +0 -0
  78. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/config.py +0 -0
  79. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/error_logging.py +0 -0
  80. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/gemini_code_assist.py +0 -0
  81. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/http_utils.py +0 -0
  82. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/keymap.py +0 -0
  83. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/main.py +0 -0
  84. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/__init__.py +0 -0
  85. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/async_lifecycle.py +0 -0
  86. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/blocking_startup.py +0 -0
  87. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  88. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/circuit_breaker.py +0 -0
  89. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/config_wizard.py +0 -0
  90. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/dashboard.py +0 -0
  91. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/error_isolation.py +0 -0
  92. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/examples/retry_example.py +0 -0
  93. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/health_monitor.py +0 -0
  94. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/managed_server.py +0 -0
  95. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/manager.py +0 -0
  96. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/mcp_logs.py +0 -0
  97. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/registry.py +0 -0
  98. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/retry_manager.py +0 -0
  99. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  100. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/status_tracker.py +0 -0
  101. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/mcp_/system_tools.py +0 -0
  102. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/__init__.py +0 -0
  103. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/bus.py +0 -0
  104. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/commands.py +0 -0
  105. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/markdown_patches.py +0 -0
  106. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/message_queue.py +0 -0
  107. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/messages.py +0 -0
  108. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/queue_console.py +0 -0
  109. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/renderers.py +0 -0
  110. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/rich_renderer.py +0 -0
  111. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/spinner/__init__.py +0 -0
  112. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  113. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  114. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/model_factory.py +0 -0
  115. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/model_utils.py +0 -0
  116. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/models.json +0 -0
  117. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/models_dev_api.json +0 -0
  118. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/models_dev_parser.py +0 -0
  119. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/__init__.py +0 -0
  120. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/__init__.py +0 -0
  121. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/accounts.py +0 -0
  122. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/antigravity_model.py +0 -0
  123. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/config.py +0 -0
  124. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/constants.py +0 -0
  125. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/oauth.py +0 -0
  126. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/register_callbacks.py +0 -0
  127. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/storage.py +0 -0
  128. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/test_plugin.py +0 -0
  129. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/token.py +0 -0
  130. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/transport.py +0 -0
  131. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/antigravity_oauth/utils.py +0 -0
  132. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/chatgpt_oauth/__init__.py +0 -0
  133. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/chatgpt_oauth/config.py +0 -0
  134. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/chatgpt_oauth/oauth_flow.py +0 -0
  135. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/chatgpt_oauth/register_callbacks.py +0 -0
  136. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/chatgpt_oauth/test_plugin.py +0 -0
  137. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/chatgpt_oauth/utils.py +0 -0
  138. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/claude_code_oauth/README.md +0 -0
  139. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/claude_code_oauth/SETUP.md +0 -0
  140. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/claude_code_oauth/__init__.py +0 -0
  141. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/claude_code_oauth/config.py +0 -0
  142. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/claude_code_oauth/register_callbacks.py +0 -0
  143. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/claude_code_oauth/test_plugin.py +0 -0
  144. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/claude_code_oauth/utils.py +0 -0
  145. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/customizable_commands/__init__.py +0 -0
  146. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/customizable_commands/register_callbacks.py +0 -0
  147. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/example_custom_command/README.md +0 -0
  148. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/example_custom_command/register_callbacks.py +0 -0
  149. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/file_permission_handler/__init__.py +0 -0
  150. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/file_permission_handler/register_callbacks.py +0 -0
  151. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  152. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/shell_safety/__init__.py +0 -0
  153. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/shell_safety/agent_shell_safety.py +0 -0
  154. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/shell_safety/command_cache.py +0 -0
  155. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/plugins/shell_safety/register_callbacks.py +0 -0
  156. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/prompts/codex_system_prompt.md +0 -0
  157. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/pydantic_patches.py +0 -0
  158. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/reopenable_async_client.py +0 -0
  159. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/round_robin_model.py +0 -0
  160. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/session_storage.py +0 -0
  161. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/status_display.py +0 -0
  162. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/summarization_agent.py +0 -0
  163. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/terminal_utils.py +0 -0
  164. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/__init__.py +0 -0
  165. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/agent_tools.py +0 -0
  166. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/browser/__init__.py +0 -0
  167. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/browser/browser_control.py +0 -0
  168. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/browser/browser_interactions.py +0 -0
  169. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/browser/browser_locators.py +0 -0
  170. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/browser/browser_navigation.py +0 -0
  171. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  172. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/browser/browser_scripts.py +0 -0
  173. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/browser/browser_workflows.py +0 -0
  174. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  175. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/browser/vqa_agent.py +0 -0
  176. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/command_runner.py +0 -0
  177. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/common.py +0 -0
  178. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/file_modifications.py +0 -0
  179. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/file_operations.py +0 -0
  180. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/tools/tools_content.py +0 -0
  181. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/uvx_detection.py +0 -0
  182. {code_puppy-0.0.340 → code_puppy-0.0.342}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.340
3
+ Version: 0.0.342
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -913,6 +913,11 @@ class BaseAgent(ABC):
913
913
  """
914
914
  Truncate message history to manage token usage.
915
915
 
916
+ Protects:
917
+ - The first message (system prompt) - always kept
918
+ - The second message if it contains a ThinkingPart (extended thinking context)
919
+ - The most recent messages up to protected_tokens
920
+
916
921
  Args:
917
922
  messages: List of messages to truncate
918
923
  protected_tokens: Number of tokens to protect
@@ -924,12 +929,30 @@ class BaseAgent(ABC):
924
929
 
925
930
  emit_info("Truncating message history to manage token usage")
926
931
  result = [messages[0]] # Always keep the first message (system prompt)
932
+
933
+ # Check if second message exists and contains a ThinkingPart
934
+ # If so, protect it (extended thinking context shouldn't be lost)
935
+ skip_second = False
936
+ if len(messages) > 1:
937
+ second_msg = messages[1]
938
+ has_thinking = any(
939
+ isinstance(part, ThinkingPart) for part in second_msg.parts
940
+ )
941
+ if has_thinking:
942
+ result.append(second_msg)
943
+ skip_second = True
944
+
927
945
  num_tokens = 0
928
946
  stack = queue.LifoQueue()
929
947
 
948
+ # Determine which messages to consider for the recent-tokens window
949
+ # Skip first message (already added), and skip second if it has thinking
950
+ start_idx = 2 if skip_second else 1
951
+ messages_to_scan = messages[start_idx:]
952
+
930
953
  # Put messages in reverse order (most recent first) into the stack
931
954
  # but break when we exceed protected_tokens
932
- for idx, msg in enumerate(reversed(messages[1:])): # Skip the first message
955
+ for msg in reversed(messages_to_scan):
933
956
  num_tokens += self.estimate_tokens_for_message(msg)
934
957
  if num_tokens > protected_tokens:
935
958
  break
@@ -9,14 +9,19 @@ serialization, avoiding httpx/Pydantic internals.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import base64
12
13
  import json
13
14
  import logging
15
+ import time
14
16
  from typing import Any, Callable, MutableMapping
15
17
 
16
18
  import httpx
17
19
 
18
20
  logger = logging.getLogger(__name__)
19
21
 
22
+ # Refresh token if it's older than 1 hour (3600 seconds)
23
+ TOKEN_MAX_AGE_SECONDS = 3600
24
+
20
25
  try:
21
26
  from anthropic import AsyncAnthropic
22
27
  except ImportError: # pragma: no cover - optional dep
@@ -24,9 +29,108 @@ except ImportError: # pragma: no cover - optional dep
24
29
 
25
30
 
26
31
  class ClaudeCacheAsyncClient(httpx.AsyncClient):
32
+ def _get_jwt_age_seconds(self, token: str | None) -> float | None:
33
+ """Decode a JWT and return its age in seconds.
34
+
35
+ Returns None if the token can't be decoded or has no timestamp claims.
36
+ Uses 'iat' (issued at) if available, otherwise calculates from 'exp'.
37
+ """
38
+ if not token:
39
+ return None
40
+
41
+ try:
42
+ # JWT format: header.payload.signature
43
+ # We only need the payload (second part)
44
+ parts = token.split(".")
45
+ if len(parts) != 3:
46
+ return None
47
+
48
+ # Decode the payload (base64url encoded)
49
+ payload_b64 = parts[1]
50
+ # Add padding if needed (base64url doesn't require padding)
51
+ padding = 4 - len(payload_b64) % 4
52
+ if padding != 4:
53
+ payload_b64 += "=" * padding
54
+
55
+ payload_bytes = base64.urlsafe_b64decode(payload_b64)
56
+ payload = json.loads(payload_bytes.decode("utf-8"))
57
+
58
+ now = time.time()
59
+
60
+ # Prefer 'iat' (issued at) claim if available
61
+ if "iat" in payload:
62
+ iat = float(payload["iat"])
63
+ age = now - iat
64
+ return age
65
+
66
+ # Fall back to calculating from 'exp' claim
67
+ # Assume tokens are typically valid for 1 hour
68
+ if "exp" in payload:
69
+ exp = float(payload["exp"])
70
+ # If exp is in the future, calculate how long until expiry
71
+ # and assume the token was issued 1 hour before expiry
72
+ time_until_exp = exp - now
73
+ # If token has less than 1 hour left, it's "old"
74
+ age = TOKEN_MAX_AGE_SECONDS - time_until_exp
75
+ return max(0, age)
76
+
77
+ return None
78
+ except Exception as exc:
79
+ logger.debug("Failed to decode JWT age: %s", exc)
80
+ return None
81
+
82
+ def _extract_bearer_token(self, request: httpx.Request) -> str | None:
83
+ """Extract the bearer token from request headers."""
84
+ auth_header = request.headers.get("Authorization") or request.headers.get(
85
+ "authorization"
86
+ )
87
+ if auth_header and auth_header.lower().startswith("bearer "):
88
+ return auth_header[7:] # Strip "Bearer " prefix
89
+ return None
90
+
91
+ def _should_refresh_token(self, request: httpx.Request) -> bool:
92
+ """Check if the token in the request is older than 1 hour."""
93
+ token = self._extract_bearer_token(request)
94
+ if not token:
95
+ return False
96
+
97
+ age = self._get_jwt_age_seconds(token)
98
+ if age is None:
99
+ return False
100
+
101
+ should_refresh = age >= TOKEN_MAX_AGE_SECONDS
102
+ if should_refresh:
103
+ logger.info(
104
+ "JWT token is %.1f seconds old (>= %d), will refresh proactively",
105
+ age,
106
+ TOKEN_MAX_AGE_SECONDS,
107
+ )
108
+ return should_refresh
109
+
27
110
  async def send(
28
111
  self, request: httpx.Request, *args: Any, **kwargs: Any
29
112
  ) -> httpx.Response: # type: ignore[override]
113
+ # Proactive token refresh: check JWT age before every request
114
+ if not request.extensions.get("claude_oauth_refresh_attempted"):
115
+ try:
116
+ if self._should_refresh_token(request):
117
+ refreshed_token = self._refresh_claude_oauth_token()
118
+ if refreshed_token:
119
+ logger.info("Proactively refreshed token before request")
120
+ # Rebuild request with new token
121
+ headers = dict(request.headers)
122
+ self._update_auth_headers(headers, refreshed_token)
123
+ body_bytes = self._extract_body_bytes(request)
124
+ request = self.build_request(
125
+ method=request.method,
126
+ url=request.url,
127
+ headers=headers,
128
+ content=body_bytes,
129
+ )
130
+ request.extensions["claude_oauth_refresh_attempted"] = True
131
+ except Exception as exc:
132
+ logger.debug("Error during proactive token refresh check: %s", exc)
133
+
30
134
  try:
31
135
  if request.url.path.endswith("/v1/messages"):
32
136
  body_bytes = self._extract_body_bytes(request)
@@ -69,12 +69,21 @@ def _get_session_entries(base_dir: Path) -> List[Tuple[str, dict]]:
69
69
 
70
70
 
71
71
  def _extract_last_user_message(history: list) -> str:
72
- """Extract the most recent user message from history."""
72
+ """Extract the most recent user message from history.
73
+
74
+ Joins all content parts from the message since messages can have
75
+ multiple parts (e.g., text + attachments, multi-part prompts).
76
+ """
73
77
  # Walk backwards through history to find last user message
74
78
  for msg in reversed(history):
79
+ content_parts = []
75
80
  for part in msg.parts:
76
81
  if hasattr(part, "content"):
77
- return part.content
82
+ content = part.content
83
+ if isinstance(content, str) and content.strip():
84
+ content_parts.append(content)
85
+ if content_parts:
86
+ return "\n\n".join(content_parts)
78
87
  return "[No messages found]"
79
88
 
80
89
 
@@ -298,19 +307,13 @@ def _render_message_browser_panel(
298
307
  # Don't override Rich's ANSI styling - use empty style
299
308
  text_color = ""
300
309
 
301
- # Truncate if too long (max 35 lines)
302
- message_lines = rendered.split("\n")[:35]
303
- is_truncated = len(rendered.split("\n")) > 35
310
+ # Show full message without truncation
311
+ message_lines = rendered.split("\n")
304
312
 
305
313
  for line in message_lines:
306
314
  lines.append((text_color, f" {line}"))
307
315
  lines.append(("", "\n"))
308
316
 
309
- if is_truncated:
310
- lines.append(("", "\n"))
311
- lines.append(("fg:yellow", " ... truncated (message too long)"))
312
- lines.append(("", "\n"))
313
-
314
317
  except Exception as e:
315
318
  lines.append(("fg:red", f" Error rendering message: {e}"))
316
319
  lines.append(("", "\n"))
@@ -359,7 +362,7 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
359
362
  lines.append(("", "\n\n"))
360
363
 
361
364
  lines.append(("bold", " Last Message:"))
362
- lines.append(("fg:ansibrightblack", " (press 'e' to browse all)"))
365
+ lines.append(("fg:ansibrightblack", " (press 'e' to browse full history)"))
363
366
  lines.append(("", "\n"))
364
367
 
365
368
  # Try to load and preview the last message
@@ -367,15 +370,11 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
367
370
  history = load_session(session_name, base_dir)
368
371
  last_message = _extract_last_user_message(history)
369
372
 
370
- # Check if original message is long (before Rich processing)
371
- original_lines = last_message.split("\n") if last_message else []
372
- is_long = len(original_lines) > 30
373
-
374
- # Render markdown with rich but strip ANSI codes
373
+ # Render markdown with rich
375
374
  console = Console(
376
375
  file=StringIO(),
377
376
  legacy_windows=False,
378
- no_color=False, # Disable ANSI color codes
377
+ no_color=False,
379
378
  force_terminal=False,
380
379
  width=76,
381
380
  )
@@ -383,19 +382,14 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
383
382
  console.print(md)
384
383
  rendered = console.file.getvalue()
385
384
 
386
- # Truncate if too long (max 30 lines for bigger preview)
387
- message_lines = rendered.split("\n")[:30]
385
+ # Show full message without truncation
386
+ message_lines = rendered.split("\n")
388
387
 
389
388
  for line in message_lines:
390
389
  # Rich already rendered the markdown, just display it dimmed
391
390
  lines.append(("fg:ansibrightblack", f" {line}"))
392
391
  lines.append(("", "\n"))
393
392
 
394
- if is_long:
395
- lines.append(("", "\n"))
396
- lines.append(("fg:yellow", " ... truncated"))
397
- lines.append(("", "\n"))
398
-
399
393
  except Exception as e:
400
394
  lines.append(("fg:red", f" Error loading preview: {e}"))
401
395
  lines.append(("", "\n"))
@@ -29,7 +29,6 @@ from code_puppy.command_line.attachments import (
29
29
  )
30
30
  from code_puppy.command_line.clipboard import (
31
31
  capture_clipboard_image_to_pending,
32
- get_clipboard_manager,
33
32
  has_image_in_clipboard,
34
33
  )
35
34
  from code_puppy.command_line.command_registry import get_unique_commands
@@ -649,30 +648,18 @@ async def get_input_with_combined_completion(
649
648
  else:
650
649
  event.current_buffer.validate_and_handle()
651
650
 
652
- # Handle bracketed paste (triggered by most terminal Cmd+V / Ctrl+V)
653
- # This is the PRIMARY paste handler - works with Cmd+V on macOS terminals
651
+ # Handle bracketed paste - TEXT ONLY, never check clipboard for images.
652
+ # Drag-and-drop sends file paths through this handler (as text), and paste
653
+ # operations send text through here too. We should NOT conflate this with
654
+ # clipboard image capture - those are separate operations (Ctrl+V / F3).
654
655
  @bindings.add(Keys.BracketedPaste)
655
656
  def handle_bracketed_paste(event):
656
- """Handle bracketed paste - works with Cmd+V on macOS terminals."""
657
- # The pasted data is in event.data
657
+ """Handle bracketed paste - insert text data only."""
658
658
  pasted_data = event.data
659
-
660
- # Check if clipboard has an image (the pasted text might just be empty or a file path)
661
- try:
662
- if has_image_in_clipboard():
663
- placeholder = capture_clipboard_image_to_pending()
664
- if placeholder:
665
- event.app.current_buffer.insert_text(placeholder + " ")
666
- count = get_clipboard_manager().get_pending_count()
667
- sys.stdout.write(f"\033[36m📋 +image ({count})\033[0m ")
668
- sys.stdout.flush()
669
- return # Don't also paste the text data
670
- except Exception:
671
- pass
672
-
673
- # No image - insert the pasted text as normal
674
659
  if pasted_data:
675
- event.app.current_buffer.insert_text(pasted_data)
660
+ # Normalize Windows line endings to Unix style
661
+ sanitized_data = pasted_data.replace("\r\n", "\n").replace("\r", "\n")
662
+ event.app.current_buffer.insert_text(sanitized_data)
676
663
 
677
664
  # Fallback Ctrl+V for terminals without bracketed paste support
678
665
  @bindings.add("c-v", eager=True)
@@ -684,9 +671,9 @@ async def get_input_with_combined_completion(
684
671
  placeholder = capture_clipboard_image_to_pending()
685
672
  if placeholder:
686
673
  event.app.current_buffer.insert_text(placeholder + " ")
687
- count = get_clipboard_manager().get_pending_count()
688
- sys.stdout.write(f"\033[36m📋 +image ({count})\033[0m ")
689
- sys.stdout.flush()
674
+ # The placeholder itself is visible feedback - no need for extra output
675
+ # Use bell for audible feedback (works in most terminals)
676
+ event.app.output.bell()
690
677
  return # Don't also paste text
691
678
  except Exception:
692
679
  pass # Fall through to text paste on any error
@@ -715,7 +702,7 @@ async def get_input_with_combined_completion(
715
702
  timeout=2,
716
703
  )
717
704
  if result.returncode == 0:
718
- text = result.stdout.rstrip("\r\n")
705
+ text = result.stdout
719
706
  else: # Linux
720
707
  # Try xclip first, then xsel
721
708
  for cmd in [
@@ -733,6 +720,10 @@ async def get_input_with_combined_completion(
733
720
  continue
734
721
 
735
722
  if text:
723
+ # Normalize Windows line endings to Unix style
724
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
725
+ # Strip trailing newline that clipboard tools often add
726
+ text = text.rstrip("\n")
736
727
  event.app.current_buffer.insert_text(text)
737
728
  except Exception:
738
729
  pass # Silently fail if text paste doesn't work
@@ -746,15 +737,16 @@ async def get_input_with_combined_completion(
746
737
  placeholder = capture_clipboard_image_to_pending()
747
738
  if placeholder:
748
739
  event.app.current_buffer.insert_text(placeholder + " ")
749
- count = get_clipboard_manager().get_pending_count()
750
- sys.stdout.write(f"\033[36m📋 +image ({count})\033[0m ")
751
- sys.stdout.flush()
740
+ # The placeholder itself is visible feedback
741
+ # Use bell for audible feedback (works in most terminals)
742
+ event.app.output.bell()
752
743
  else:
753
- sys.stdout.write("\033[33m⚠️ no image\033[0m ")
754
- sys.stdout.flush()
744
+ # Insert a transient message that user can delete
745
+ event.app.current_buffer.insert_text("[⚠️ no image in clipboard] ")
746
+ event.app.output.bell()
755
747
  except Exception:
756
- sys.stdout.write("\033[31m❌ clipboard error\033[0m ")
757
- sys.stdout.flush()
748
+ event.app.current_buffer.insert_text("[❌ clipboard error] ")
749
+ event.app.output.bell()
758
750
 
759
751
  session = PromptSession(
760
752
  completer=completer,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.340"
7
+ version = "0.0.342"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.14"
File without changes
File without changes
File without changes