code-puppy 0.0.166__tar.gz → 0.0.167__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 (112) hide show
  1. {code_puppy-0.0.166 → code_puppy-0.0.167}/PKG-INFO +2 -1
  2. code_puppy-0.0.167/code_puppy/http_utils.py +225 -0
  3. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/managed_server.py +2 -4
  4. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/server_registry_catalog.py +5 -6
  5. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tools/command_runner.py +45 -10
  6. {code_puppy-0.0.166 → code_puppy-0.0.167}/pyproject.toml +2 -1
  7. code_puppy-0.0.166/code_puppy/http_utils.py +0 -122
  8. {code_puppy-0.0.166 → code_puppy-0.0.167}/.gitignore +0 -0
  9. {code_puppy-0.0.166 → code_puppy-0.0.167}/LICENSE +0 -0
  10. {code_puppy-0.0.166 → code_puppy-0.0.167}/README.md +0 -0
  11. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/__init__.py +0 -0
  12. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/__main__.py +0 -0
  13. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/agent.py +0 -0
  14. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/agents/__init__.py +0 -0
  15. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/agents/agent_code_puppy.py +0 -0
  16. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/agents/agent_creator_agent.py +0 -0
  17. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/agents/agent_manager.py +0 -0
  18. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/agents/agent_orchestrator.json +0 -0
  19. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/agents/base_agent.py +0 -0
  20. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/agents/json_agent.py +0 -0
  21. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/agents/runtime_manager.py +0 -0
  22. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/callbacks.py +0 -0
  23. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/__init__.py +0 -0
  24. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/command_handler.py +0 -0
  25. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/file_path_completion.py +0 -0
  26. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/load_context_completion.py +0 -0
  27. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/__init__.py +0 -0
  28. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/add_command.py +0 -0
  29. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/base.py +0 -0
  30. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/handler.py +0 -0
  31. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/help_command.py +0 -0
  32. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/install_command.py +0 -0
  33. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/list_command.py +0 -0
  34. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/logs_command.py +0 -0
  35. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/remove_command.py +0 -0
  36. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/restart_command.py +0 -0
  37. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/search_command.py +0 -0
  38. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  39. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/start_command.py +0 -0
  40. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/status_command.py +0 -0
  41. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  42. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/stop_command.py +0 -0
  43. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/test_command.py +0 -0
  44. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/utils.py +0 -0
  45. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  46. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/meta_command_handler.py +0 -0
  47. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/model_picker_completion.py +0 -0
  48. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/motd.py +0 -0
  49. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  50. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/command_line/utils.py +0 -0
  51. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/config.py +0 -0
  52. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/main.py +0 -0
  53. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/__init__.py +0 -0
  54. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/async_lifecycle.py +0 -0
  55. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/blocking_startup.py +0 -0
  56. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/captured_stdio_server.py +0 -0
  57. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/circuit_breaker.py +0 -0
  58. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/config_wizard.py +0 -0
  59. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/dashboard.py +0 -0
  60. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/error_isolation.py +0 -0
  61. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/examples/retry_example.py +0 -0
  62. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/health_monitor.py +0 -0
  63. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/manager.py +0 -0
  64. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/registry.py +0 -0
  65. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/retry_manager.py +0 -0
  66. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/status_tracker.py +0 -0
  67. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/mcp/system_tools.py +0 -0
  68. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/message_history_processor.py +0 -0
  69. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/messaging/__init__.py +0 -0
  70. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/messaging/message_queue.py +0 -0
  71. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/messaging/queue_console.py +0 -0
  72. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/messaging/renderers.py +0 -0
  73. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/messaging/spinner/__init__.py +0 -0
  74. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  75. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  76. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  77. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/model_factory.py +0 -0
  78. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/models.json +0 -0
  79. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/plugins/__init__.py +0 -0
  80. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/reopenable_async_client.py +0 -0
  81. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/round_robin_model.py +0 -0
  82. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/state_management.py +0 -0
  83. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/status_display.py +0 -0
  84. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/summarization_agent.py +0 -0
  85. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tools/__init__.py +0 -0
  86. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tools/agent_tools.py +0 -0
  87. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tools/common.py +0 -0
  88. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tools/file_modifications.py +0 -0
  89. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tools/file_operations.py +0 -0
  90. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tools/tools_content.py +0 -0
  91. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/__init__.py +0 -0
  92. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/app.py +0 -0
  93. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/components/__init__.py +0 -0
  94. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/components/chat_view.py +0 -0
  95. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/components/command_history_modal.py +0 -0
  96. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/components/copy_button.py +0 -0
  97. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/components/custom_widgets.py +0 -0
  98. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/components/human_input_modal.py +0 -0
  99. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/components/input_area.py +0 -0
  100. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/components/sidebar.py +0 -0
  101. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/components/status_bar.py +0 -0
  102. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/messages.py +0 -0
  103. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/models/__init__.py +0 -0
  104. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/models/chat_message.py +0 -0
  105. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/models/command_history.py +0 -0
  106. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/models/enums.py +0 -0
  107. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/screens/__init__.py +0 -0
  108. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/screens/help.py +0 -0
  109. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
  110. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/screens/settings.py +0 -0
  111. {code_puppy-0.0.166 → code_puppy-0.0.167}/code_puppy/tui/screens/tools.py +0 -0
  112. {code_puppy-0.0.166 → code_puppy-0.0.167}/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.166
3
+ Version: 0.0.167
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
@@ -33,6 +33,7 @@ Requires-Dist: rapidfuzz>=3.13.0
33
33
  Requires-Dist: rich>=13.4.2
34
34
  Requires-Dist: ripgrep>=14.1.0
35
35
  Requires-Dist: ruff>=0.11.11
36
+ Requires-Dist: tenacity>=8.2.0
36
37
  Requires-Dist: termcolor>=3.1.0
37
38
  Requires-Dist: textual-dev>=1.7.0
38
39
  Requires-Dist: textual>=5.0.0
@@ -0,0 +1,225 @@
1
+ """
2
+ HTTP utilities module for code-puppy.
3
+
4
+ This module provides functions for creating properly configured HTTP clients.
5
+ """
6
+
7
+ import os
8
+ import socket
9
+ from typing import Dict, Optional, Union
10
+
11
+ import httpx
12
+ import requests
13
+ from tenacity import retry_if_exception_type, stop_after_attempt, wait_exponential
14
+
15
+ try:
16
+ from pydantic_ai.retries import (
17
+ AsyncTenacityTransport,
18
+ RetryConfig,
19
+ TenacityTransport,
20
+ wait_retry_after,
21
+ )
22
+ except ImportError:
23
+ # Fallback if pydantic_ai.retries is not available
24
+ AsyncTenacityTransport = None
25
+ RetryConfig = None
26
+ TenacityTransport = None
27
+ wait_retry_after = None
28
+
29
+ try:
30
+ from .reopenable_async_client import ReopenableAsyncClient
31
+ except ImportError:
32
+ ReopenableAsyncClient = None
33
+
34
+ try:
35
+ from .messaging import emit_info
36
+ except ImportError:
37
+ # Fallback if messaging system is not available
38
+ def emit_info(content: str, **metadata):
39
+ pass # No-op if messaging system is not available
40
+
41
+
42
+ def get_cert_bundle_path() -> str:
43
+ # First check if SSL_CERT_FILE environment variable is set
44
+ ssl_cert_file = os.environ.get("SSL_CERT_FILE")
45
+ if ssl_cert_file and os.path.exists(ssl_cert_file):
46
+ return ssl_cert_file
47
+
48
+
49
+ def create_client(
50
+ timeout: int = 180,
51
+ verify: Union[bool, str] = None,
52
+ headers: Optional[Dict[str, str]] = None,
53
+ retry_status_codes: tuple = (429, 502, 503, 504),
54
+ ) -> httpx.Client:
55
+ if verify is None:
56
+ verify = get_cert_bundle_path()
57
+
58
+ # If retry components are available, create a client with retry transport
59
+ if TenacityTransport and RetryConfig and wait_retry_after:
60
+ def should_retry_status(response):
61
+ """Raise exceptions for retryable HTTP status codes."""
62
+ if response.status_code in retry_status_codes:
63
+ emit_info(f"HTTP retry: Retrying request due to status code {response.status_code}")
64
+ response.raise_for_status()
65
+
66
+ transport = TenacityTransport(
67
+ config=RetryConfig(
68
+ retry=lambda e: isinstance(e, httpx.HTTPStatusError) and e.response.status_code in retry_status_codes,
69
+ wait=wait_retry_after(
70
+ fallback_strategy=wait_exponential(multiplier=1, max=60),
71
+ max_wait=300
72
+ ),
73
+ stop=stop_after_attempt(5),
74
+ reraise=True
75
+ ),
76
+ validate_response=should_retry_status
77
+ )
78
+
79
+ return httpx.Client(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
80
+ else:
81
+ # Fallback to regular client if retry components are not available
82
+ return httpx.Client(verify=verify, headers=headers or {}, timeout=timeout)
83
+
84
+
85
+ def create_async_client(
86
+ timeout: int = 180,
87
+ verify: Union[bool, str] = None,
88
+ headers: Optional[Dict[str, str]] = None,
89
+ retry_status_codes: tuple = (429, 502, 503, 504),
90
+ ) -> httpx.AsyncClient:
91
+ if verify is None:
92
+ verify = get_cert_bundle_path()
93
+
94
+ # If retry components are available, create a client with retry transport
95
+ if AsyncTenacityTransport and RetryConfig and wait_retry_after:
96
+ def should_retry_status(response):
97
+ """Raise exceptions for retryable HTTP status codes."""
98
+ if response.status_code in retry_status_codes:
99
+ emit_info(f"HTTP retry: Retrying request due to status code {response.status_code}")
100
+ response.raise_for_status()
101
+
102
+ transport = AsyncTenacityTransport(
103
+ config=RetryConfig(
104
+ retry=lambda e: isinstance(e, httpx.HTTPStatusError) and e.response.status_code in retry_status_codes,
105
+ wait=wait_retry_after(
106
+ fallback_strategy=wait_exponential(multiplier=1, max=60),
107
+ max_wait=300
108
+ ),
109
+ stop=stop_after_attempt(5),
110
+ reraise=True
111
+ ),
112
+ validate_response=should_retry_status
113
+ )
114
+
115
+ return httpx.AsyncClient(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
116
+ else:
117
+ # Fallback to regular client if retry components are not available
118
+ return httpx.AsyncClient(verify=verify, headers=headers or {}, timeout=timeout)
119
+
120
+
121
+ def create_requests_session(
122
+ timeout: float = 5.0,
123
+ verify: Union[bool, str] = None,
124
+ headers: Optional[Dict[str, str]] = None,
125
+ ) -> requests.Session:
126
+ session = requests.Session()
127
+
128
+ if verify is None:
129
+ verify = get_cert_bundle_path()
130
+
131
+ session.verify = verify
132
+
133
+ if headers:
134
+ session.headers.update(headers or {})
135
+
136
+ return session
137
+
138
+
139
+ def create_auth_headers(
140
+ api_key: str, header_name: str = "Authorization"
141
+ ) -> Dict[str, str]:
142
+ return {header_name: f"Bearer {api_key}"}
143
+
144
+
145
+ def resolve_env_var_in_header(headers: Dict[str, str]) -> Dict[str, str]:
146
+ resolved_headers = {}
147
+
148
+ for key, value in headers.items():
149
+ if isinstance(value, str):
150
+ try:
151
+ expanded = os.path.expandvars(value)
152
+ resolved_headers[key] = expanded
153
+ except Exception:
154
+ resolved_headers[key] = value
155
+ else:
156
+ resolved_headers[key] = value
157
+
158
+ return resolved_headers
159
+
160
+
161
+ def create_reopenable_async_client(
162
+ timeout: int = 180,
163
+ verify: Union[bool, str] = None,
164
+ headers: Optional[Dict[str, str]] = None,
165
+ retry_status_codes: tuple = (429, 502, 503, 504),
166
+ ) -> Union[ReopenableAsyncClient, httpx.AsyncClient]:
167
+ if verify is None:
168
+ verify = get_cert_bundle_path()
169
+
170
+ # If retry components are available, create a client with retry transport
171
+ if AsyncTenacityTransport and RetryConfig and wait_retry_after:
172
+ def should_retry_status(response):
173
+ """Raise exceptions for retryable HTTP status codes."""
174
+ if response.status_code in retry_status_codes:
175
+ emit_info(f"HTTP retry: Retrying request due to status code {response.status_code}")
176
+ response.raise_for_status()
177
+
178
+ transport = AsyncTenacityTransport(
179
+ config=RetryConfig(
180
+ retry=lambda e: isinstance(e, httpx.HTTPStatusError) and e.response.status_code in retry_status_codes,
181
+ wait=wait_retry_after(
182
+ fallback_strategy=wait_exponential(multiplier=1, max=60),
183
+ max_wait=300
184
+ ),
185
+ stop=stop_after_attempt(5),
186
+ reraise=True
187
+ ),
188
+ validate_response=should_retry_status
189
+ )
190
+
191
+ if ReopenableAsyncClient is not None:
192
+ return ReopenableAsyncClient(
193
+ transport=transport, verify=verify, headers=headers or {}, timeout=timeout
194
+ )
195
+ else:
196
+ # Fallback to regular AsyncClient if ReopenableAsyncClient is not available
197
+ return httpx.AsyncClient(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
198
+ else:
199
+ # Fallback to regular clients if retry components are not available
200
+ if ReopenableAsyncClient is not None:
201
+ return ReopenableAsyncClient(
202
+ verify=verify, headers=headers or {}, timeout=timeout
203
+ )
204
+ else:
205
+ # Fallback to regular AsyncClient if ReopenableAsyncClient is not available
206
+ return httpx.AsyncClient(verify=verify, headers=headers or {}, timeout=timeout)
207
+
208
+
209
+ def is_cert_bundle_available() -> bool:
210
+ cert_path = get_cert_bundle_path()
211
+ return os.path.exists(cert_path) and os.path.isfile(cert_path)
212
+
213
+
214
+ def find_available_port(start_port=8090, end_port=9010, host="127.0.0.1"):
215
+ for port in range(start_port, end_port + 1):
216
+ try:
217
+ # Try to bind to the port to check if it's available
218
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
219
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
220
+ sock.bind((host, port))
221
+ return port
222
+ except OSError:
223
+ # Port is in use, try the next one
224
+ continue
225
+ return None
@@ -226,11 +226,9 @@ class ManagedMCPServer:
226
226
  http_kwargs["timeout"] = config["timeout"]
227
227
  if "read_timeout" in config:
228
228
  http_kwargs["read_timeout"] = config["read_timeout"]
229
- if "http_client" in config:
230
- http_kwargs["http_client"] = config["http_client"]
231
- elif config.get("headers"):
229
+ if "headers" in config:
230
+ http_kwargs["headers"] = config.get("headers")
232
231
  # Create HTTP client if headers are provided but no client specified
233
- http_kwargs["http_client"] = self._get_http_client()
234
232
 
235
233
  self._pydantic_server = MCPServerStreamableHTTP(
236
234
  **http_kwargs, process_tool_call=process_tool_call
@@ -791,18 +791,17 @@ MCP_SERVER_REGISTRY: List[MCPServerTemplate] = [
791
791
  description="Search and retrieve documentation from multiple sources with AI-powered context understanding",
792
792
  category="Documentation",
793
793
  tags=["documentation", "search", "context", "ai", "knowledge", "docs", "cloud"],
794
- type="stdio",
794
+ type="http",
795
795
  config={
796
- "timeout": 30,
797
- "command": "npx",
798
- "args": ["-y", "@upstash/context7-mcp", "--api-key", "$CONTEXT7_API_KEY"],
796
+ "url": "https://mcp.context7.com/mcp",
797
+ "headers": {
798
+ "Authorization": "Bearer $CONTEXT7_API_KEY"
799
+ }
799
800
  },
800
801
  verified=True,
801
802
  popular=True,
802
803
  requires=MCPServerRequirements(
803
804
  environment_vars=["CONTEXT7_API_KEY"],
804
- required_tools=["node", "npx"],
805
- package_dependencies=["@upstash/context7-mcp"],
806
805
  ),
807
806
  example_usage="Cloud-based service - no local setup required",
808
807
  ),
@@ -22,6 +22,17 @@ from code_puppy.messaging import (
22
22
  from code_puppy.state_management import is_tui_mode
23
23
  from code_puppy.tools.common import generate_group_id
24
24
 
25
+ # Maximum line length for shell command output to prevent massive token usage
26
+ # This helps avoid exceeding model context limits when commands produce very long lines
27
+ MAX_LINE_LENGTH = 256
28
+
29
+
30
+ def _truncate_line(line: str) -> str:
31
+ """Truncate a line to MAX_LINE_LENGTH if it exceeds the limit."""
32
+ if len(line) > MAX_LINE_LENGTH:
33
+ return line[:MAX_LINE_LENGTH] + "... [truncated]"
34
+ return line
35
+
25
36
  _AWAITING_USER_INPUT = False
26
37
 
27
38
  _CONFIRMATION_LOCK = threading.Lock()
@@ -188,6 +199,8 @@ def run_shell_command_streaming(
188
199
  for line in iter(process.stdout.readline, ""):
189
200
  if line:
190
201
  line = line.rstrip("\n\r")
202
+ # Limit line length to prevent massive token usage
203
+ line = _truncate_line(line)
191
204
  stdout_lines.append(line)
192
205
  emit_system_message(line, message_group=group_id)
193
206
  last_output_time[0] = time.time()
@@ -199,6 +212,8 @@ def run_shell_command_streaming(
199
212
  for line in iter(process.stderr.readline, ""):
200
213
  if line:
201
214
  line = line.rstrip("\n\r")
215
+ # Limit line length to prevent massive token usage
216
+ line = _truncate_line(line)
202
217
  stderr_lines.append(line)
203
218
  emit_system_message(line, message_group=group_id)
204
219
  last_output_time[0] = time.time()
@@ -252,8 +267,8 @@ def run_shell_command_streaming(
252
267
  **{
253
268
  "success": False,
254
269
  "command": command,
255
- "stdout": "\n".join(stdout_lines[-1000:]),
256
- "stderr": "\n".join(stderr_lines[-1000:]),
270
+ "stdout": "\n".join(stdout_lines[-256:]),
271
+ "stderr": "\n".join(stderr_lines[-256:]),
257
272
  "exit_code": -9,
258
273
  "execution_time": execution_time,
259
274
  "timeout": True,
@@ -315,23 +330,31 @@ def run_shell_command_streaming(
315
330
  )
316
331
  emit_info(f"Took {execution_time:.2f}s", message_group=group_id)
317
332
  time.sleep(1)
333
+ # Apply line length limits to stdout/stderr before returning
334
+ truncated_stdout = [_truncate_line(line) for line in stdout_lines[-256:]]
335
+ truncated_stderr = [_truncate_line(line) for line in stderr_lines[-256:]]
336
+
318
337
  return ShellCommandOutput(
319
338
  success=False,
320
339
  command=command,
321
340
  error="""The process didn't exit cleanly! If the user_interrupted flag is true,
322
341
  please stop all execution and ask the user for clarification!""",
323
- stdout="\n".join(stdout_lines[-1000:]),
324
- stderr="\n".join(stderr_lines[-1000:]),
342
+ stdout="\n".join(truncated_stdout),
343
+ stderr="\n".join(truncated_stderr),
325
344
  exit_code=exit_code,
326
345
  execution_time=execution_time,
327
346
  timeout=False,
328
347
  user_interrupted=process.pid in _USER_KILLED_PROCESSES,
329
348
  )
349
+ # Apply line length limits to stdout/stderr before returning
350
+ truncated_stdout = [_truncate_line(line) for line in stdout_lines[-256:]]
351
+ truncated_stderr = [_truncate_line(line) for line in stderr_lines[-256:]]
352
+
330
353
  return ShellCommandOutput(
331
354
  success=exit_code == 0,
332
355
  command=command,
333
- stdout="\n".join(stdout_lines[-1000:]),
334
- stderr="\n".join(stderr_lines[-1000:]),
356
+ stdout="\n".join(truncated_stdout),
357
+ stderr="\n".join(truncated_stderr),
335
358
  exit_code=exit_code,
336
359
  execution_time=execution_time,
337
360
  timeout=False,
@@ -453,12 +476,24 @@ def run_shell_command(
453
476
  stdout = None
454
477
  if "stderr" not in locals():
455
478
  stderr = None
479
+
480
+ # Apply line length limits to stdout/stderr if they exist
481
+ truncated_stdout = None
482
+ if stdout:
483
+ stdout_lines = stdout.split("\n")
484
+ truncated_stdout = "\n".join([_truncate_line(line) for line in stdout_lines[-256:]])
485
+
486
+ truncated_stderr = None
487
+ if stderr:
488
+ stderr_lines = stderr.split("\n")
489
+ truncated_stderr = "\n".join([_truncate_line(line) for line in stderr_lines[-256:]])
490
+
456
491
  return ShellCommandOutput(
457
492
  success=False,
458
493
  command=command,
459
494
  error=f"Error executing command {str(e)}",
460
- stdout="\n".join(stdout[-1000:]) if stdout else None,
461
- stderr="\n".join(stderr[-1000:]) if stderr else None,
495
+ stdout=truncated_stdout,
496
+ stderr=truncated_stderr,
462
497
  exit_code=-1,
463
498
  timeout=False,
464
499
  )
@@ -520,8 +555,8 @@ def register_agent_run_shell_command(agent):
520
555
  - success (bool): True if command executed successfully (exit code 0)
521
556
  - command (str | None): The executed command string
522
557
  - error (str | None): Error message if execution failed
523
- - stdout (str | None): Standard output from the command (last 1000 lines)
524
- - stderr (str | None): Standard error from the command (last 1000 lines)
558
+ - stdout (str | None): Standard output from the command (last 256 lines)
559
+ - stderr (str | None): Standard error from the command (last 256 lines)
525
560
  - exit_code (int | None): Process exit code
526
561
  - execution_time (float | None): Total execution time in seconds
527
562
  - timeout (bool | None): True if command was terminated due to timeout
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.166"
7
+ version = "0.0.167"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -33,6 +33,7 @@ dependencies = [
33
33
  "textual-dev>=1.7.0",
34
34
  "openai>=1.99.1",
35
35
  "ripgrep>=14.1.0",
36
+ "tenacity>=8.2.0",
36
37
  ]
37
38
  dev-dependencies = [
38
39
  "pytest>=8.3.4",
@@ -1,122 +0,0 @@
1
- """
2
- HTTP utilities module for code-puppy.
3
-
4
- This module provides functions for creating properly configured HTTP clients.
5
- """
6
-
7
- import os
8
- import socket
9
- from typing import Dict, Optional, Union
10
-
11
- import httpx
12
- import requests
13
-
14
- try:
15
- from .reopenable_async_client import ReopenableAsyncClient
16
- except ImportError:
17
- ReopenableAsyncClient = None
18
-
19
-
20
- def get_cert_bundle_path() -> str:
21
- # First check if SSL_CERT_FILE environment variable is set
22
- ssl_cert_file = os.environ.get("SSL_CERT_FILE")
23
- if ssl_cert_file and os.path.exists(ssl_cert_file):
24
- return ssl_cert_file
25
-
26
-
27
- def create_client(
28
- timeout: int = 180,
29
- verify: Union[bool, str] = None,
30
- headers: Optional[Dict[str, str]] = None,
31
- ) -> httpx.Client:
32
- if verify is None:
33
- verify = get_cert_bundle_path()
34
-
35
- return httpx.Client(verify=verify, headers=headers or {}, timeout=timeout)
36
-
37
-
38
- def create_async_client(
39
- timeout: int = 180,
40
- verify: Union[bool, str] = None,
41
- headers: Optional[Dict[str, str]] = None,
42
- ) -> httpx.AsyncClient:
43
- if verify is None:
44
- verify = get_cert_bundle_path()
45
-
46
- return httpx.AsyncClient(verify=verify, headers=headers or {}, timeout=timeout)
47
-
48
-
49
- def create_requests_session(
50
- timeout: float = 5.0,
51
- verify: Union[bool, str] = None,
52
- headers: Optional[Dict[str, str]] = None,
53
- ) -> requests.Session:
54
- session = requests.Session()
55
-
56
- if verify is None:
57
- verify = get_cert_bundle_path()
58
-
59
- session.verify = verify
60
-
61
- if headers:
62
- session.headers.update(headers or {})
63
-
64
- return session
65
-
66
-
67
- def create_auth_headers(
68
- api_key: str, header_name: str = "Authorization"
69
- ) -> Dict[str, str]:
70
- return {header_name: f"Bearer {api_key}"}
71
-
72
-
73
- def resolve_env_var_in_header(headers: Dict[str, str]) -> Dict[str, str]:
74
- resolved_headers = {}
75
-
76
- for key, value in headers.items():
77
- if isinstance(value, str):
78
- try:
79
- expanded = os.path.expandvars(value)
80
- resolved_headers[key] = expanded
81
- except Exception:
82
- resolved_headers[key] = value
83
- else:
84
- resolved_headers[key] = value
85
-
86
- return resolved_headers
87
-
88
-
89
- def create_reopenable_async_client(
90
- timeout: int = 180,
91
- verify: Union[bool, str] = None,
92
- headers: Optional[Dict[str, str]] = None,
93
- ) -> Union[ReopenableAsyncClient, httpx.AsyncClient]:
94
- if verify is None:
95
- verify = get_cert_bundle_path()
96
-
97
- if ReopenableAsyncClient is not None:
98
- return ReopenableAsyncClient(
99
- verify=verify, headers=headers or {}, timeout=timeout
100
- )
101
- else:
102
- # Fallback to regular AsyncClient if ReopenableAsyncClient is not available
103
- return httpx.AsyncClient(verify=verify, headers=headers or {}, timeout=timeout)
104
-
105
-
106
- def is_cert_bundle_available() -> bool:
107
- cert_path = get_cert_bundle_path()
108
- return os.path.exists(cert_path) and os.path.isfile(cert_path)
109
-
110
-
111
- def find_available_port(start_port=8090, end_port=9010, host="127.0.0.1"):
112
- for port in range(start_port, end_port + 1):
113
- try:
114
- # Try to bind to the port to check if it's available
115
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
116
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
117
- sock.bind((host, port))
118
- return port
119
- except OSError:
120
- # Port is in use, try the next one
121
- continue
122
- return None
File without changes
File without changes
File without changes