tunacode-cli 0.1.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/__init__.py +0 -0
- tunacode/cli/textual_repl.tcss +283 -0
- tunacode/configuration/__init__.py +1 -0
- tunacode/configuration/defaults.py +45 -0
- tunacode/configuration/models.py +147 -0
- tunacode/configuration/models_registry.json +1 -0
- tunacode/configuration/pricing.py +74 -0
- tunacode/configuration/settings.py +35 -0
- tunacode/constants.py +227 -0
- tunacode/core/__init__.py +6 -0
- tunacode/core/agents/__init__.py +39 -0
- tunacode/core/agents/agent_components/__init__.py +48 -0
- tunacode/core/agents/agent_components/agent_config.py +441 -0
- tunacode/core/agents/agent_components/agent_helpers.py +290 -0
- tunacode/core/agents/agent_components/message_handler.py +99 -0
- tunacode/core/agents/agent_components/node_processor.py +477 -0
- tunacode/core/agents/agent_components/response_state.py +129 -0
- tunacode/core/agents/agent_components/result_wrapper.py +51 -0
- tunacode/core/agents/agent_components/state_transition.py +112 -0
- tunacode/core/agents/agent_components/streaming.py +271 -0
- tunacode/core/agents/agent_components/task_completion.py +40 -0
- tunacode/core/agents/agent_components/tool_buffer.py +44 -0
- tunacode/core/agents/agent_components/tool_executor.py +101 -0
- tunacode/core/agents/agent_components/truncation_checker.py +37 -0
- tunacode/core/agents/delegation_tools.py +109 -0
- tunacode/core/agents/main.py +545 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/research_agent.py +231 -0
- tunacode/core/compaction.py +218 -0
- tunacode/core/prompting/__init__.py +27 -0
- tunacode/core/prompting/loader.py +66 -0
- tunacode/core/prompting/prompting_engine.py +98 -0
- tunacode/core/prompting/sections.py +50 -0
- tunacode/core/prompting/templates.py +69 -0
- tunacode/core/state.py +409 -0
- tunacode/exceptions.py +313 -0
- tunacode/indexing/__init__.py +5 -0
- tunacode/indexing/code_index.py +432 -0
- tunacode/indexing/constants.py +86 -0
- tunacode/lsp/__init__.py +112 -0
- tunacode/lsp/client.py +351 -0
- tunacode/lsp/diagnostics.py +19 -0
- tunacode/lsp/servers.py +101 -0
- tunacode/prompts/default_prompt.md +952 -0
- tunacode/prompts/research/sections/agent_role.xml +5 -0
- tunacode/prompts/research/sections/constraints.xml +14 -0
- tunacode/prompts/research/sections/output_format.xml +57 -0
- tunacode/prompts/research/sections/tool_use.xml +23 -0
- tunacode/prompts/sections/advanced_patterns.xml +255 -0
- tunacode/prompts/sections/agent_role.xml +8 -0
- tunacode/prompts/sections/completion.xml +10 -0
- tunacode/prompts/sections/critical_rules.xml +37 -0
- tunacode/prompts/sections/examples.xml +220 -0
- tunacode/prompts/sections/output_style.xml +94 -0
- tunacode/prompts/sections/parallel_exec.xml +105 -0
- tunacode/prompts/sections/search_pattern.xml +100 -0
- tunacode/prompts/sections/system_info.xml +6 -0
- tunacode/prompts/sections/tool_use.xml +84 -0
- tunacode/prompts/sections/user_instructions.xml +3 -0
- tunacode/py.typed +0 -0
- tunacode/templates/__init__.py +5 -0
- tunacode/templates/loader.py +15 -0
- tunacode/tools/__init__.py +10 -0
- tunacode/tools/authorization/__init__.py +29 -0
- tunacode/tools/authorization/context.py +32 -0
- tunacode/tools/authorization/factory.py +20 -0
- tunacode/tools/authorization/handler.py +58 -0
- tunacode/tools/authorization/notifier.py +35 -0
- tunacode/tools/authorization/policy.py +19 -0
- tunacode/tools/authorization/requests.py +119 -0
- tunacode/tools/authorization/rules.py +72 -0
- tunacode/tools/bash.py +222 -0
- tunacode/tools/decorators.py +213 -0
- tunacode/tools/glob.py +353 -0
- tunacode/tools/grep.py +468 -0
- tunacode/tools/grep_components/__init__.py +9 -0
- tunacode/tools/grep_components/file_filter.py +93 -0
- tunacode/tools/grep_components/pattern_matcher.py +158 -0
- tunacode/tools/grep_components/result_formatter.py +87 -0
- tunacode/tools/grep_components/search_result.py +34 -0
- tunacode/tools/list_dir.py +205 -0
- tunacode/tools/prompts/bash_prompt.xml +10 -0
- tunacode/tools/prompts/glob_prompt.xml +7 -0
- tunacode/tools/prompts/grep_prompt.xml +10 -0
- tunacode/tools/prompts/list_dir_prompt.xml +7 -0
- tunacode/tools/prompts/read_file_prompt.xml +9 -0
- tunacode/tools/prompts/todoclear_prompt.xml +12 -0
- tunacode/tools/prompts/todoread_prompt.xml +16 -0
- tunacode/tools/prompts/todowrite_prompt.xml +28 -0
- tunacode/tools/prompts/update_file_prompt.xml +9 -0
- tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
- tunacode/tools/prompts/write_file_prompt.xml +7 -0
- tunacode/tools/react.py +111 -0
- tunacode/tools/read_file.py +68 -0
- tunacode/tools/todo.py +222 -0
- tunacode/tools/update_file.py +62 -0
- tunacode/tools/utils/__init__.py +1 -0
- tunacode/tools/utils/ripgrep.py +311 -0
- tunacode/tools/utils/text_match.py +352 -0
- tunacode/tools/web_fetch.py +245 -0
- tunacode/tools/write_file.py +34 -0
- tunacode/tools/xml_helper.py +34 -0
- tunacode/types/__init__.py +166 -0
- tunacode/types/base.py +94 -0
- tunacode/types/callbacks.py +53 -0
- tunacode/types/dataclasses.py +121 -0
- tunacode/types/pydantic_ai.py +31 -0
- tunacode/types/state.py +122 -0
- tunacode/ui/__init__.py +6 -0
- tunacode/ui/app.py +542 -0
- tunacode/ui/commands/__init__.py +430 -0
- tunacode/ui/components/__init__.py +1 -0
- tunacode/ui/headless/__init__.py +5 -0
- tunacode/ui/headless/output.py +72 -0
- tunacode/ui/main.py +252 -0
- tunacode/ui/renderers/__init__.py +41 -0
- tunacode/ui/renderers/errors.py +197 -0
- tunacode/ui/renderers/panels.py +550 -0
- tunacode/ui/renderers/search.py +314 -0
- tunacode/ui/renderers/tools/__init__.py +21 -0
- tunacode/ui/renderers/tools/bash.py +247 -0
- tunacode/ui/renderers/tools/diagnostics.py +186 -0
- tunacode/ui/renderers/tools/glob.py +226 -0
- tunacode/ui/renderers/tools/grep.py +228 -0
- tunacode/ui/renderers/tools/list_dir.py +198 -0
- tunacode/ui/renderers/tools/read_file.py +226 -0
- tunacode/ui/renderers/tools/research.py +294 -0
- tunacode/ui/renderers/tools/update_file.py +237 -0
- tunacode/ui/renderers/tools/web_fetch.py +182 -0
- tunacode/ui/repl_support.py +226 -0
- tunacode/ui/screens/__init__.py +16 -0
- tunacode/ui/screens/model_picker.py +303 -0
- tunacode/ui/screens/session_picker.py +181 -0
- tunacode/ui/screens/setup.py +218 -0
- tunacode/ui/screens/theme_picker.py +90 -0
- tunacode/ui/screens/update_confirm.py +69 -0
- tunacode/ui/shell_runner.py +129 -0
- tunacode/ui/styles/layout.tcss +98 -0
- tunacode/ui/styles/modals.tcss +38 -0
- tunacode/ui/styles/panels.tcss +81 -0
- tunacode/ui/styles/theme-nextstep.tcss +303 -0
- tunacode/ui/styles/widgets.tcss +33 -0
- tunacode/ui/styles.py +18 -0
- tunacode/ui/widgets/__init__.py +23 -0
- tunacode/ui/widgets/command_autocomplete.py +62 -0
- tunacode/ui/widgets/editor.py +402 -0
- tunacode/ui/widgets/file_autocomplete.py +47 -0
- tunacode/ui/widgets/messages.py +46 -0
- tunacode/ui/widgets/resource_bar.py +182 -0
- tunacode/ui/widgets/status_bar.py +98 -0
- tunacode/utils/__init__.py +0 -0
- tunacode/utils/config/__init__.py +13 -0
- tunacode/utils/config/user_configuration.py +91 -0
- tunacode/utils/messaging/__init__.py +10 -0
- tunacode/utils/messaging/message_utils.py +34 -0
- tunacode/utils/messaging/token_counter.py +77 -0
- tunacode/utils/parsing/__init__.py +13 -0
- tunacode/utils/parsing/command_parser.py +55 -0
- tunacode/utils/parsing/json_utils.py +188 -0
- tunacode/utils/parsing/retry.py +146 -0
- tunacode/utils/parsing/tool_parser.py +267 -0
- tunacode/utils/security/__init__.py +15 -0
- tunacode/utils/security/command.py +106 -0
- tunacode/utils/system/__init__.py +25 -0
- tunacode/utils/system/gitignore.py +155 -0
- tunacode/utils/system/paths.py +190 -0
- tunacode/utils/ui/__init__.py +9 -0
- tunacode/utils/ui/file_filter.py +135 -0
- tunacode/utils/ui/helpers.py +24 -0
- tunacode_cli-0.1.21.dist-info/METADATA +170 -0
- tunacode_cli-0.1.21.dist-info/RECORD +174 -0
- tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
- tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
- tunacode_cli-0.1.21.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Web fetch tool for TunaCode - HTTP GET requests with HTML-to-text conversion.
|
|
3
|
+
|
|
4
|
+
This tool provides web content fetching with:
|
|
5
|
+
- HTTP GET requests with configurable timeout
|
|
6
|
+
- HTML-to-text conversion for readable output
|
|
7
|
+
- URL security validation (blocks localhost, private IPs, file://)
|
|
8
|
+
- Content size limiting (5MB max)
|
|
9
|
+
|
|
10
|
+
CLAUDE_ANCHOR[web-fetch-module]: HTTP GET with HTML-to-text conversion
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ipaddress
|
|
14
|
+
import re
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
import html2text
|
|
18
|
+
import httpx
|
|
19
|
+
from pydantic_ai.exceptions import ModelRetry
|
|
20
|
+
|
|
21
|
+
from tunacode.tools.decorators import base_tool
|
|
22
|
+
|
|
23
|
+
# Constants
|
|
24
|
+
MAX_CONTENT_SIZE = 5 * 1024 * 1024 # 5MB
|
|
25
|
+
MAX_OUTPUT_SIZE = 100 * 1024 # 100KB for output truncation
|
|
26
|
+
DEFAULT_TIMEOUT = 60 # seconds
|
|
27
|
+
USER_AGENT = "TunaCode/1.0 (https://tunacode.xyz)"
|
|
28
|
+
|
|
29
|
+
# Private IP ranges to block
|
|
30
|
+
PRIVATE_IP_PATTERNS = [
|
|
31
|
+
re.compile(r"^127\."), # 127.x.x.x
|
|
32
|
+
re.compile(r"^10\."), # 10.x.x.x
|
|
33
|
+
re.compile(r"^172\.(1[6-9]|2[0-9]|3[01])\."), # 172.16-31.x.x
|
|
34
|
+
re.compile(r"^192\.168\."), # 192.168.x.x
|
|
35
|
+
re.compile(r"^0\."), # 0.x.x.x
|
|
36
|
+
re.compile(r"^169\.254\."), # Link-local
|
|
37
|
+
re.compile(r"^::1$"), # IPv6 localhost
|
|
38
|
+
re.compile(r"^fe80:"), # IPv6 link-local
|
|
39
|
+
re.compile(r"^fc00:"), # IPv6 unique local
|
|
40
|
+
re.compile(r"^fd00:"), # IPv6 unique local
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# Blocked hostnames
|
|
44
|
+
BLOCKED_HOSTNAMES = frozenset(
|
|
45
|
+
[
|
|
46
|
+
"localhost",
|
|
47
|
+
"localhost.localdomain",
|
|
48
|
+
"local",
|
|
49
|
+
"0.0.0.0", # nosec B104 - this is a blocklist, not a bind address
|
|
50
|
+
"127.0.0.1",
|
|
51
|
+
"::1",
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_private_ip(ip_str: str) -> bool:
|
|
57
|
+
"""Check if an IP address is private/reserved."""
|
|
58
|
+
for pattern in PRIVATE_IP_PATTERNS:
|
|
59
|
+
if pattern.match(ip_str):
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
ip = ipaddress.ip_address(ip_str)
|
|
64
|
+
return ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local
|
|
65
|
+
except ValueError:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _validate_url(url: str) -> str:
|
|
70
|
+
"""Validate URL for security.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
url: URL to validate
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Validated URL
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ModelRetry: If URL is invalid or blocked
|
|
80
|
+
"""
|
|
81
|
+
if not url or not url.strip():
|
|
82
|
+
raise ModelRetry("URL cannot be empty.")
|
|
83
|
+
|
|
84
|
+
url = url.strip()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
parsed = urlparse(url)
|
|
88
|
+
except Exception as err:
|
|
89
|
+
raise ModelRetry(f"Invalid URL format: {url}") from err
|
|
90
|
+
|
|
91
|
+
# Check scheme
|
|
92
|
+
if parsed.scheme not in ("http", "https"):
|
|
93
|
+
raise ModelRetry(
|
|
94
|
+
f"Invalid URL scheme '{parsed.scheme}'. Only http:// and https:// are allowed."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Check hostname presence
|
|
98
|
+
if not parsed.hostname:
|
|
99
|
+
raise ModelRetry(f"URL missing hostname: {url}")
|
|
100
|
+
|
|
101
|
+
hostname = parsed.hostname.lower()
|
|
102
|
+
|
|
103
|
+
# Block known localhost hostnames
|
|
104
|
+
if hostname in BLOCKED_HOSTNAMES:
|
|
105
|
+
raise ModelRetry(f"Blocked URL: {url}. Cannot fetch from localhost or local addresses.")
|
|
106
|
+
|
|
107
|
+
# Check if hostname is an IP address and validate
|
|
108
|
+
if _is_private_ip(hostname):
|
|
109
|
+
raise ModelRetry(f"Blocked URL: {url}. Cannot fetch from private or reserved IP addresses.")
|
|
110
|
+
|
|
111
|
+
return url
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _convert_html_to_text(html_content: str) -> str:
|
|
115
|
+
"""Convert HTML to readable plain text.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
html_content: Raw HTML content
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Plain text extracted from HTML
|
|
122
|
+
"""
|
|
123
|
+
converter = html2text.HTML2Text()
|
|
124
|
+
converter.ignore_links = False
|
|
125
|
+
converter.ignore_images = True
|
|
126
|
+
converter.ignore_emphasis = False
|
|
127
|
+
converter.body_width = 80
|
|
128
|
+
converter.unicode_snob = True
|
|
129
|
+
converter.skip_internal_links = True
|
|
130
|
+
|
|
131
|
+
return converter.handle(html_content)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _truncate_output(content: str, max_size: int = MAX_OUTPUT_SIZE) -> str:
|
|
135
|
+
"""Truncate content if it exceeds max size.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
content: Content to truncate
|
|
139
|
+
max_size: Maximum size in bytes
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Truncated content with indicator if truncated
|
|
143
|
+
"""
|
|
144
|
+
if len(content.encode("utf-8")) <= max_size:
|
|
145
|
+
return content
|
|
146
|
+
|
|
147
|
+
# Truncate to approximate character count
|
|
148
|
+
truncated = content[: max_size // 2]
|
|
149
|
+
return truncated + "\n\n... [Content truncated due to size] ..."
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@base_tool
|
|
153
|
+
async def web_fetch(
|
|
154
|
+
url: str,
|
|
155
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
156
|
+
) -> str:
|
|
157
|
+
"""Fetch web content from a URL and return as readable text.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
url: The URL to fetch (http:// or https://)
|
|
161
|
+
timeout: Request timeout in seconds (default: 60)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Readable text content from the URL
|
|
165
|
+
"""
|
|
166
|
+
# Validate URL security
|
|
167
|
+
validated_url = _validate_url(url)
|
|
168
|
+
|
|
169
|
+
# Clamp timeout to reasonable bounds
|
|
170
|
+
timeout = max(5, min(timeout, 120))
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
async with httpx.AsyncClient(
|
|
174
|
+
timeout=httpx.Timeout(timeout),
|
|
175
|
+
follow_redirects=True,
|
|
176
|
+
max_redirects=5,
|
|
177
|
+
headers={"User-Agent": USER_AGENT},
|
|
178
|
+
) as client:
|
|
179
|
+
# First, do a HEAD request to check content size
|
|
180
|
+
try:
|
|
181
|
+
head_response = await client.head(validated_url)
|
|
182
|
+
content_length = head_response.headers.get("content-length")
|
|
183
|
+
if content_length and int(content_length) > MAX_CONTENT_SIZE:
|
|
184
|
+
raise ModelRetry(
|
|
185
|
+
f"Content too large ({int(content_length) // 1024 // 1024}MB). "
|
|
186
|
+
f"Maximum allowed is {MAX_CONTENT_SIZE // 1024 // 1024}MB."
|
|
187
|
+
)
|
|
188
|
+
except httpx.HTTPError:
|
|
189
|
+
# HEAD failed, proceed with GET and stream check
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
# Fetch the actual content
|
|
193
|
+
response = await client.get(validated_url)
|
|
194
|
+
response.raise_for_status()
|
|
195
|
+
|
|
196
|
+
# Check final URL after redirects for security
|
|
197
|
+
final_url = str(response.url)
|
|
198
|
+
if final_url != validated_url:
|
|
199
|
+
_validate_url(final_url)
|
|
200
|
+
|
|
201
|
+
# Check content size
|
|
202
|
+
content = response.content
|
|
203
|
+
if len(content) > MAX_CONTENT_SIZE:
|
|
204
|
+
raise ModelRetry(
|
|
205
|
+
f"Content too large ({len(content) // 1024 // 1024}MB). "
|
|
206
|
+
f"Maximum allowed is {MAX_CONTENT_SIZE // 1024 // 1024}MB."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Decode content
|
|
210
|
+
try:
|
|
211
|
+
text_content = content.decode("utf-8")
|
|
212
|
+
except UnicodeDecodeError:
|
|
213
|
+
text_content = content.decode("latin-1", errors="replace")
|
|
214
|
+
|
|
215
|
+
# Convert HTML to text if content is HTML
|
|
216
|
+
content_type = response.headers.get("content-type", "").lower()
|
|
217
|
+
if "text/html" in content_type or "<html" in text_content[:1000].lower():
|
|
218
|
+
text_content = _convert_html_to_text(text_content)
|
|
219
|
+
|
|
220
|
+
# Truncate if too large
|
|
221
|
+
text_content = _truncate_output(text_content)
|
|
222
|
+
|
|
223
|
+
return text_content
|
|
224
|
+
|
|
225
|
+
except httpx.TimeoutException as err:
|
|
226
|
+
msg = f"Request timed out after {timeout} seconds. Try again or use a shorter timeout."
|
|
227
|
+
raise ModelRetry(msg) from err
|
|
228
|
+
except httpx.TooManyRedirects as err:
|
|
229
|
+
msg = f"Too many redirects while fetching {url}. The URL may be invalid."
|
|
230
|
+
raise ModelRetry(msg) from err
|
|
231
|
+
except httpx.HTTPStatusError as err:
|
|
232
|
+
status = err.response.status_code
|
|
233
|
+
if status == 404:
|
|
234
|
+
raise ModelRetry(f"Page not found (404): {url}. Check the URL.") from err
|
|
235
|
+
if status == 403:
|
|
236
|
+
msg = f"Access forbidden (403): {url}. The page may require authentication."
|
|
237
|
+
raise ModelRetry(msg) from err
|
|
238
|
+
if status == 429:
|
|
239
|
+
raise ModelRetry(f"Rate limited (429): {url}. Try again later.") from err
|
|
240
|
+
if status >= 500:
|
|
241
|
+
msg = f"Server error ({status}): {url}. The server may be down."
|
|
242
|
+
raise ModelRetry(msg) from err
|
|
243
|
+
raise ModelRetry(f"HTTP error {status} fetching {url}") from err
|
|
244
|
+
except httpx.RequestError as err:
|
|
245
|
+
raise ModelRetry(f"Failed to connect to {url}: {err}") from err
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""File writing tool for agent operations."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from pydantic_ai.exceptions import ModelRetry
|
|
6
|
+
|
|
7
|
+
from tunacode.tools.decorators import file_tool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@file_tool(writes=True)
|
|
11
|
+
async def write_file(filepath: str, content: str) -> str:
|
|
12
|
+
"""Write content to a new file. Fails if the file already exists.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
filepath: The absolute path to the file to write.
|
|
16
|
+
content: The content to write to the file.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
A message indicating success.
|
|
20
|
+
"""
|
|
21
|
+
if os.path.exists(filepath):
|
|
22
|
+
raise ModelRetry(
|
|
23
|
+
f"File '{filepath}' already exists. "
|
|
24
|
+
"Use the `update_file` tool to modify it, or choose a different filepath."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
dirpath = os.path.dirname(filepath)
|
|
28
|
+
if dirpath and not os.path.exists(dirpath):
|
|
29
|
+
os.makedirs(dirpath, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
32
|
+
f.write(content)
|
|
33
|
+
|
|
34
|
+
return f"Successfully wrote to new file: {filepath}"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Helper module for loading prompts and schemas from XML files."""
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from defusedxml.ElementTree import ParseError
|
|
7
|
+
from defusedxml.ElementTree import parse as xml_parse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@lru_cache(maxsize=32)
|
|
11
|
+
def load_prompt_from_xml(tool_name: str) -> str | None:
|
|
12
|
+
"""Load and return the base prompt from XML file.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
tool_name: Name of the tool (e.g., 'grep', 'glob')
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
str: The loaded prompt from XML or None if not found
|
|
19
|
+
"""
|
|
20
|
+
prompt_file = Path(__file__).parent / "prompts" / f"{tool_name}_prompt.xml"
|
|
21
|
+
if not prompt_file.exists():
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
tree = xml_parse(prompt_file)
|
|
26
|
+
except ParseError:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
root = tree.getroot()
|
|
30
|
+
description = root.find("description")
|
|
31
|
+
if description is None or description.text is None:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
return description.text.strip()
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Centralized type definitions for TunaCode CLI.
|
|
2
|
+
|
|
3
|
+
This package contains all type aliases, protocols, and type definitions
|
|
4
|
+
used throughout the TunaCode codebase.
|
|
5
|
+
|
|
6
|
+
All types are re-exported from this module for backward compatibility.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Awaitable, Callable
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
# Base types
|
|
13
|
+
from tunacode.types.base import (
|
|
14
|
+
AgentConfig,
|
|
15
|
+
AgentName,
|
|
16
|
+
CommandArgs,
|
|
17
|
+
CommandResult,
|
|
18
|
+
ConfigFile,
|
|
19
|
+
ConfigPath,
|
|
20
|
+
CostAmount,
|
|
21
|
+
DeviceId,
|
|
22
|
+
DiffHunk,
|
|
23
|
+
DiffLine,
|
|
24
|
+
EnvConfig,
|
|
25
|
+
ErrorContext,
|
|
26
|
+
ErrorMessage,
|
|
27
|
+
FileContent,
|
|
28
|
+
FileDiff,
|
|
29
|
+
FileEncoding,
|
|
30
|
+
FilePath,
|
|
31
|
+
FileSize,
|
|
32
|
+
InputSessions,
|
|
33
|
+
LineNumber,
|
|
34
|
+
ModelName,
|
|
35
|
+
OriginalError,
|
|
36
|
+
SessionId,
|
|
37
|
+
TokenCount,
|
|
38
|
+
ToolArgs,
|
|
39
|
+
ToolCallId,
|
|
40
|
+
ToolName,
|
|
41
|
+
ToolResult,
|
|
42
|
+
UpdateOperation,
|
|
43
|
+
UserConfig,
|
|
44
|
+
ValidationResult,
|
|
45
|
+
Validator,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Callback types
|
|
49
|
+
from tunacode.types.callbacks import (
|
|
50
|
+
AsyncFunc,
|
|
51
|
+
AsyncToolFunc,
|
|
52
|
+
AsyncVoidFunc,
|
|
53
|
+
ToolCallback,
|
|
54
|
+
ToolProgress,
|
|
55
|
+
ToolProgressCallback,
|
|
56
|
+
ToolStartCallback,
|
|
57
|
+
UICallback,
|
|
58
|
+
UIInputCallback,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Dataclasses
|
|
62
|
+
from tunacode.types.dataclasses import (
|
|
63
|
+
AgentState,
|
|
64
|
+
CommandContext,
|
|
65
|
+
CostBreakdown,
|
|
66
|
+
FallbackResponse,
|
|
67
|
+
ModelConfig,
|
|
68
|
+
ModelPricing,
|
|
69
|
+
ModelRegistry,
|
|
70
|
+
ResponseState,
|
|
71
|
+
TokenUsage,
|
|
72
|
+
ToolConfirmationRequest,
|
|
73
|
+
ToolConfirmationResponse,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Pydantic-AI wrappers
|
|
77
|
+
from tunacode.types.pydantic_ai import (
|
|
78
|
+
AgentResponse,
|
|
79
|
+
AgentRun,
|
|
80
|
+
MessageHistory,
|
|
81
|
+
MessagePart,
|
|
82
|
+
ModelRequest,
|
|
83
|
+
ModelResponse,
|
|
84
|
+
PydanticAgent,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# State protocol
|
|
88
|
+
from tunacode.types.state import (
|
|
89
|
+
SessionStateProtocol,
|
|
90
|
+
StateManager,
|
|
91
|
+
StateManagerProtocol,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Backward compatibility: ProcessRequestCallback needs StateManager
|
|
95
|
+
ProcessRequestCallback = Callable[[str, StateManager, bool], Awaitable[Any]]
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
# Base types
|
|
99
|
+
"AgentConfig",
|
|
100
|
+
"AgentName",
|
|
101
|
+
"CommandArgs",
|
|
102
|
+
"CommandResult",
|
|
103
|
+
"ConfigFile",
|
|
104
|
+
"ConfigPath",
|
|
105
|
+
"CostAmount",
|
|
106
|
+
"DeviceId",
|
|
107
|
+
"DiffHunk",
|
|
108
|
+
"DiffLine",
|
|
109
|
+
"EnvConfig",
|
|
110
|
+
"ErrorContext",
|
|
111
|
+
"ErrorMessage",
|
|
112
|
+
"FileContent",
|
|
113
|
+
"FileDiff",
|
|
114
|
+
"FileEncoding",
|
|
115
|
+
"FilePath",
|
|
116
|
+
"FileSize",
|
|
117
|
+
"InputSessions",
|
|
118
|
+
"LineNumber",
|
|
119
|
+
"ModelName",
|
|
120
|
+
"OriginalError",
|
|
121
|
+
"SessionId",
|
|
122
|
+
"TokenCount",
|
|
123
|
+
"ToolArgs",
|
|
124
|
+
"ToolCallId",
|
|
125
|
+
"ToolName",
|
|
126
|
+
"ToolResult",
|
|
127
|
+
"UpdateOperation",
|
|
128
|
+
"UserConfig",
|
|
129
|
+
"ValidationResult",
|
|
130
|
+
"Validator",
|
|
131
|
+
# Pydantic-AI
|
|
132
|
+
"AgentResponse",
|
|
133
|
+
"AgentRun",
|
|
134
|
+
"MessageHistory",
|
|
135
|
+
"MessagePart",
|
|
136
|
+
"ModelRequest",
|
|
137
|
+
"ModelResponse",
|
|
138
|
+
"PydanticAgent",
|
|
139
|
+
# Callbacks
|
|
140
|
+
"AsyncFunc",
|
|
141
|
+
"AsyncToolFunc",
|
|
142
|
+
"AsyncVoidFunc",
|
|
143
|
+
"ProcessRequestCallback",
|
|
144
|
+
"ToolCallback",
|
|
145
|
+
"ToolProgress",
|
|
146
|
+
"ToolProgressCallback",
|
|
147
|
+
"ToolStartCallback",
|
|
148
|
+
"UICallback",
|
|
149
|
+
"UIInputCallback",
|
|
150
|
+
# State
|
|
151
|
+
"SessionStateProtocol",
|
|
152
|
+
"StateManager",
|
|
153
|
+
"StateManagerProtocol",
|
|
154
|
+
# Dataclasses
|
|
155
|
+
"AgentState",
|
|
156
|
+
"CommandContext",
|
|
157
|
+
"CostBreakdown",
|
|
158
|
+
"FallbackResponse",
|
|
159
|
+
"ModelConfig",
|
|
160
|
+
"ModelPricing",
|
|
161
|
+
"ModelRegistry",
|
|
162
|
+
"ResponseState",
|
|
163
|
+
"TokenUsage",
|
|
164
|
+
"ToolConfirmationRequest",
|
|
165
|
+
"ToolConfirmationResponse",
|
|
166
|
+
]
|
tunacode/types/base.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Base type aliases for TunaCode CLI.
|
|
2
|
+
|
|
3
|
+
Contains fundamental type definitions that have no external dependencies
|
|
4
|
+
beyond Python stdlib.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
# Identity types - string wrappers for semantic clarity
|
|
12
|
+
ModelName = str
|
|
13
|
+
ToolName = str
|
|
14
|
+
SessionId = str
|
|
15
|
+
DeviceId = str
|
|
16
|
+
AgentName = str
|
|
17
|
+
ToolCallId = str
|
|
18
|
+
|
|
19
|
+
# File system types
|
|
20
|
+
FilePath = str | Path
|
|
21
|
+
FileContent = str
|
|
22
|
+
FileEncoding = str
|
|
23
|
+
FileDiff = tuple[str, str]
|
|
24
|
+
FileSize = int
|
|
25
|
+
LineNumber = int
|
|
26
|
+
ConfigPath = Path
|
|
27
|
+
ConfigFile = Path
|
|
28
|
+
|
|
29
|
+
# Configuration types
|
|
30
|
+
UserConfig = dict[str, Any]
|
|
31
|
+
EnvConfig = dict[str, str]
|
|
32
|
+
InputSessions = dict[str, Any]
|
|
33
|
+
AgentConfig = dict[str, Any]
|
|
34
|
+
|
|
35
|
+
# Tool types
|
|
36
|
+
ToolArgs = dict[str, Any]
|
|
37
|
+
ToolResult = str
|
|
38
|
+
|
|
39
|
+
# Error handling types
|
|
40
|
+
ErrorContext = dict[str, Any]
|
|
41
|
+
OriginalError = Exception | None
|
|
42
|
+
ErrorMessage = str
|
|
43
|
+
|
|
44
|
+
# Diff types
|
|
45
|
+
UpdateOperation = dict[str, Any]
|
|
46
|
+
DiffLine = str
|
|
47
|
+
DiffHunk = list[DiffLine]
|
|
48
|
+
|
|
49
|
+
# Validation types
|
|
50
|
+
ValidationResult = bool | str
|
|
51
|
+
Validator = Callable[[Any], ValidationResult]
|
|
52
|
+
|
|
53
|
+
# Token/Cost types
|
|
54
|
+
TokenCount = int
|
|
55
|
+
CostAmount = float
|
|
56
|
+
|
|
57
|
+
# Command types
|
|
58
|
+
CommandArgs = list[str]
|
|
59
|
+
CommandResult = Any | None
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"AgentConfig",
|
|
63
|
+
"AgentName",
|
|
64
|
+
"CommandArgs",
|
|
65
|
+
"CommandResult",
|
|
66
|
+
"ConfigFile",
|
|
67
|
+
"ConfigPath",
|
|
68
|
+
"CostAmount",
|
|
69
|
+
"DeviceId",
|
|
70
|
+
"DiffHunk",
|
|
71
|
+
"DiffLine",
|
|
72
|
+
"EnvConfig",
|
|
73
|
+
"ErrorContext",
|
|
74
|
+
"ErrorMessage",
|
|
75
|
+
"FileContent",
|
|
76
|
+
"FileDiff",
|
|
77
|
+
"FileEncoding",
|
|
78
|
+
"FilePath",
|
|
79
|
+
"FileSize",
|
|
80
|
+
"InputSessions",
|
|
81
|
+
"LineNumber",
|
|
82
|
+
"ModelName",
|
|
83
|
+
"OriginalError",
|
|
84
|
+
"SessionId",
|
|
85
|
+
"TokenCount",
|
|
86
|
+
"ToolArgs",
|
|
87
|
+
"ToolCallId",
|
|
88
|
+
"ToolName",
|
|
89
|
+
"ToolResult",
|
|
90
|
+
"UpdateOperation",
|
|
91
|
+
"UserConfig",
|
|
92
|
+
"ValidationResult",
|
|
93
|
+
"Validator",
|
|
94
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Callback type definitions for TunaCode CLI.
|
|
2
|
+
|
|
3
|
+
Contains callback signatures and the ToolProgress dataclass for
|
|
4
|
+
structured progress reporting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class ToolProgress:
|
|
14
|
+
"""Structured progress information for subagent tool execution.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
subagent: Name of the subagent (e.g., "research")
|
|
18
|
+
operation: Description of current operation (e.g., "grep pattern...")
|
|
19
|
+
current: Current operation count (1-indexed)
|
|
20
|
+
total: Total expected operations (0 if unknown)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
subagent: str
|
|
24
|
+
operation: str
|
|
25
|
+
current: int
|
|
26
|
+
total: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Tool callbacks
|
|
30
|
+
ToolCallback = Callable[[Any, Any], Awaitable[None]]
|
|
31
|
+
ToolStartCallback = Callable[[str], None]
|
|
32
|
+
ToolProgressCallback = Callable[[ToolProgress], None]
|
|
33
|
+
|
|
34
|
+
# UI callbacks
|
|
35
|
+
UICallback = Callable[[str], Awaitable[None]]
|
|
36
|
+
UIInputCallback = Callable[[str, str], Awaitable[str]]
|
|
37
|
+
|
|
38
|
+
# Async function types
|
|
39
|
+
AsyncFunc = Callable[..., Awaitable[Any]]
|
|
40
|
+
AsyncToolFunc = Callable[..., Awaitable[str]]
|
|
41
|
+
AsyncVoidFunc = Callable[..., Awaitable[None]]
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"AsyncFunc",
|
|
45
|
+
"AsyncToolFunc",
|
|
46
|
+
"AsyncVoidFunc",
|
|
47
|
+
"ToolCallback",
|
|
48
|
+
"ToolProgress",
|
|
49
|
+
"ToolProgressCallback",
|
|
50
|
+
"ToolStartCallback",
|
|
51
|
+
"UICallback",
|
|
52
|
+
"UIInputCallback",
|
|
53
|
+
]
|