superqode 0.1.5__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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Network Tools - HTTP/Web Operations.
|
|
3
|
+
|
|
4
|
+
Provides tools for fetching content from URLs, making API requests,
|
|
5
|
+
and working with web resources.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Fetch HTML/JSON/text from URLs
|
|
9
|
+
- Configurable timeouts and size limits
|
|
10
|
+
- HTML to text extraction
|
|
11
|
+
- JSON response parsing
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import ssl
|
|
17
|
+
import urllib.request
|
|
18
|
+
import urllib.error
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, Optional
|
|
21
|
+
from html.parser import HTMLParser
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
from .base import Tool, ToolResult, ToolContext
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HTMLTextExtractor(HTMLParser):
|
|
28
|
+
"""Extract text content from HTML."""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self._text = []
|
|
33
|
+
self._skip_tags = {"script", "style", "head", "meta", "link"}
|
|
34
|
+
self._current_tag = None
|
|
35
|
+
self._in_skip = False
|
|
36
|
+
|
|
37
|
+
def handle_starttag(self, tag, attrs):
|
|
38
|
+
self._current_tag = tag.lower()
|
|
39
|
+
if self._current_tag in self._skip_tags:
|
|
40
|
+
self._in_skip = True
|
|
41
|
+
|
|
42
|
+
def handle_endtag(self, tag):
|
|
43
|
+
if tag.lower() in self._skip_tags:
|
|
44
|
+
self._in_skip = False
|
|
45
|
+
self._current_tag = None
|
|
46
|
+
|
|
47
|
+
def handle_data(self, data):
|
|
48
|
+
if not self._in_skip:
|
|
49
|
+
text = data.strip()
|
|
50
|
+
if text:
|
|
51
|
+
self._text.append(text)
|
|
52
|
+
|
|
53
|
+
def get_text(self) -> str:
|
|
54
|
+
return "\n".join(self._text)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FetchTool(Tool):
|
|
58
|
+
"""
|
|
59
|
+
Fetch content from URLs.
|
|
60
|
+
|
|
61
|
+
Supports:
|
|
62
|
+
- HTML pages (with text extraction)
|
|
63
|
+
- JSON APIs
|
|
64
|
+
- Plain text
|
|
65
|
+
- Raw content
|
|
66
|
+
|
|
67
|
+
Security:
|
|
68
|
+
- Configurable size limits
|
|
69
|
+
- Timeout protection
|
|
70
|
+
- No file:// URLs
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
DEFAULT_TIMEOUT = 30
|
|
74
|
+
MAX_SIZE = 1024 * 1024 # 1MB default limit
|
|
75
|
+
USER_AGENT = "SuperQode/1.0 (AI Coding Assistant)"
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def name(self) -> str:
|
|
79
|
+
return "fetch"
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def description(self) -> str:
|
|
83
|
+
return "Fetch content from a URL. Supports HTML (extracts text), JSON, and plain text."
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def parameters(self) -> Dict[str, Any]:
|
|
87
|
+
return {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {
|
|
90
|
+
"url": {"type": "string", "description": "URL to fetch (http or https)"},
|
|
91
|
+
"format": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"enum": ["auto", "text", "json", "html", "raw"],
|
|
94
|
+
"description": "Response format: auto (detect), text (extract from HTML), json, html (raw HTML), raw (bytes as text)",
|
|
95
|
+
},
|
|
96
|
+
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)"},
|
|
97
|
+
"headers": {"type": "object", "description": "Additional HTTP headers to send"},
|
|
98
|
+
"max_size": {
|
|
99
|
+
"type": "integer",
|
|
100
|
+
"description": "Maximum response size in bytes (default: 1MB)",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
"required": ["url"],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
|
|
107
|
+
url = args.get("url", "")
|
|
108
|
+
format_type = args.get("format", "auto")
|
|
109
|
+
timeout = args.get("timeout", self.DEFAULT_TIMEOUT)
|
|
110
|
+
headers = args.get("headers", {})
|
|
111
|
+
max_size = args.get("max_size", self.MAX_SIZE)
|
|
112
|
+
|
|
113
|
+
# Validate URL
|
|
114
|
+
if not url:
|
|
115
|
+
return ToolResult(success=False, output="", error="URL is required")
|
|
116
|
+
|
|
117
|
+
if not url.startswith(("http://", "https://")):
|
|
118
|
+
return ToolResult(
|
|
119
|
+
success=False, output="", error="Only http:// and https:// URLs are supported"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Run fetch in executor to not block
|
|
124
|
+
loop = asyncio.get_event_loop()
|
|
125
|
+
result = await asyncio.wait_for(
|
|
126
|
+
loop.run_in_executor(
|
|
127
|
+
None, lambda: self._sync_fetch(url, headers, timeout, max_size)
|
|
128
|
+
),
|
|
129
|
+
timeout=timeout + 5, # Extra buffer for executor
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if result.get("error"):
|
|
133
|
+
return ToolResult(success=False, output="", error=result["error"])
|
|
134
|
+
|
|
135
|
+
content = result["content"]
|
|
136
|
+
content_type = result.get("content_type", "")
|
|
137
|
+
|
|
138
|
+
# Process based on format
|
|
139
|
+
output = self._process_content(content, content_type, format_type)
|
|
140
|
+
|
|
141
|
+
return ToolResult(
|
|
142
|
+
success=True,
|
|
143
|
+
output=output,
|
|
144
|
+
metadata={
|
|
145
|
+
"url": url,
|
|
146
|
+
"content_type": content_type,
|
|
147
|
+
"size": len(content),
|
|
148
|
+
"format": format_type,
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
except asyncio.TimeoutError:
|
|
153
|
+
return ToolResult(
|
|
154
|
+
success=False, output="", error=f"Request timed out after {timeout} seconds"
|
|
155
|
+
)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return ToolResult(success=False, output="", error=f"Fetch error: {str(e)}")
|
|
158
|
+
|
|
159
|
+
def _sync_fetch(
|
|
160
|
+
self, url: str, headers: Dict[str, str], timeout: int, max_size: int
|
|
161
|
+
) -> Dict[str, Any]:
|
|
162
|
+
"""Synchronous fetch implementation."""
|
|
163
|
+
try:
|
|
164
|
+
# Build request
|
|
165
|
+
req = urllib.request.Request(url)
|
|
166
|
+
req.add_header("User-Agent", self.USER_AGENT)
|
|
167
|
+
|
|
168
|
+
for key, value in headers.items():
|
|
169
|
+
req.add_header(key, value)
|
|
170
|
+
|
|
171
|
+
# Create SSL context
|
|
172
|
+
ctx = ssl.create_default_context()
|
|
173
|
+
|
|
174
|
+
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as response:
|
|
175
|
+
content_type = response.headers.get("Content-Type", "")
|
|
176
|
+
|
|
177
|
+
# Read with size limit
|
|
178
|
+
content = response.read(max_size)
|
|
179
|
+
|
|
180
|
+
# Check if truncated
|
|
181
|
+
extra = response.read(1)
|
|
182
|
+
if extra:
|
|
183
|
+
content += b"\n\n[Content truncated at " + str(max_size).encode() + b" bytes]"
|
|
184
|
+
|
|
185
|
+
# Decode
|
|
186
|
+
charset = self._get_charset(content_type)
|
|
187
|
+
try:
|
|
188
|
+
text = content.decode(charset, errors="replace")
|
|
189
|
+
except (UnicodeDecodeError, LookupError):
|
|
190
|
+
text = content.decode("utf-8", errors="replace")
|
|
191
|
+
|
|
192
|
+
return {"content": text, "content_type": content_type}
|
|
193
|
+
|
|
194
|
+
except urllib.error.HTTPError as e:
|
|
195
|
+
return {"error": f"HTTP {e.code}: {e.reason}"}
|
|
196
|
+
except urllib.error.URLError as e:
|
|
197
|
+
return {"error": f"URL Error: {str(e.reason)}"}
|
|
198
|
+
except Exception as e:
|
|
199
|
+
return {"error": str(e)}
|
|
200
|
+
|
|
201
|
+
def _get_charset(self, content_type: str) -> str:
|
|
202
|
+
"""Extract charset from Content-Type header."""
|
|
203
|
+
if not content_type:
|
|
204
|
+
return "utf-8"
|
|
205
|
+
|
|
206
|
+
# Look for charset=
|
|
207
|
+
match = re.search(r"charset=([^\s;]+)", content_type, re.I)
|
|
208
|
+
if match:
|
|
209
|
+
return match.group(1).strip("\"'")
|
|
210
|
+
|
|
211
|
+
return "utf-8"
|
|
212
|
+
|
|
213
|
+
def _process_content(self, content: str, content_type: str, format_type: str) -> str:
|
|
214
|
+
"""Process content based on format type."""
|
|
215
|
+
# Auto-detect format
|
|
216
|
+
if format_type == "auto":
|
|
217
|
+
if "application/json" in content_type:
|
|
218
|
+
format_type = "json"
|
|
219
|
+
elif "text/html" in content_type:
|
|
220
|
+
format_type = "text" # Extract text from HTML
|
|
221
|
+
else:
|
|
222
|
+
format_type = "raw"
|
|
223
|
+
|
|
224
|
+
if format_type == "json":
|
|
225
|
+
try:
|
|
226
|
+
data = json.loads(content)
|
|
227
|
+
return json.dumps(data, indent=2)
|
|
228
|
+
except json.JSONDecodeError:
|
|
229
|
+
return content
|
|
230
|
+
|
|
231
|
+
elif format_type == "text":
|
|
232
|
+
# Extract text from HTML
|
|
233
|
+
try:
|
|
234
|
+
parser = HTMLTextExtractor()
|
|
235
|
+
parser.feed(content)
|
|
236
|
+
text = parser.get_text()
|
|
237
|
+
return text if text else content
|
|
238
|
+
except Exception:
|
|
239
|
+
return content
|
|
240
|
+
|
|
241
|
+
elif format_type == "html":
|
|
242
|
+
return content
|
|
243
|
+
|
|
244
|
+
else: # raw
|
|
245
|
+
return content
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class DownloadTool(Tool):
|
|
249
|
+
"""
|
|
250
|
+
Download a file from a URL.
|
|
251
|
+
|
|
252
|
+
Saves the file to the specified path.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
DEFAULT_TIMEOUT = 60
|
|
256
|
+
MAX_SIZE = 50 * 1024 * 1024 # 50MB limit
|
|
257
|
+
USER_AGENT = "SuperQode/1.0 (AI Coding Assistant)"
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def name(self) -> str:
|
|
261
|
+
return "download"
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def description(self) -> str:
|
|
265
|
+
return "Download a file from a URL and save it to a path."
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def parameters(self) -> Dict[str, Any]:
|
|
269
|
+
return {
|
|
270
|
+
"type": "object",
|
|
271
|
+
"properties": {
|
|
272
|
+
"url": {"type": "string", "description": "URL to download from"},
|
|
273
|
+
"path": {"type": "string", "description": "Path to save the file"},
|
|
274
|
+
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 60)"},
|
|
275
|
+
},
|
|
276
|
+
"required": ["url", "path"],
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
|
|
280
|
+
url = args.get("url", "")
|
|
281
|
+
path = args.get("path", "")
|
|
282
|
+
timeout = args.get("timeout", self.DEFAULT_TIMEOUT)
|
|
283
|
+
|
|
284
|
+
if not url or not path:
|
|
285
|
+
return ToolResult(success=False, output="", error="Both url and path are required")
|
|
286
|
+
|
|
287
|
+
if not url.startswith(("http://", "https://")):
|
|
288
|
+
return ToolResult(
|
|
289
|
+
success=False, output="", error="Only http:// and https:// URLs are supported"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
file_path = Path(path)
|
|
293
|
+
if not file_path.is_absolute():
|
|
294
|
+
file_path = ctx.working_directory / file_path
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
loop = asyncio.get_event_loop()
|
|
298
|
+
result = await asyncio.wait_for(
|
|
299
|
+
loop.run_in_executor(None, lambda: self._sync_download(url, file_path, timeout)),
|
|
300
|
+
timeout=timeout + 5,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if result.get("error"):
|
|
304
|
+
return ToolResult(success=False, output="", error=result["error"])
|
|
305
|
+
|
|
306
|
+
return ToolResult(
|
|
307
|
+
success=True,
|
|
308
|
+
output=f"Downloaded {result['size']} bytes to {path}",
|
|
309
|
+
metadata={"url": url, "path": str(file_path), "size": result["size"]},
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
except asyncio.TimeoutError:
|
|
313
|
+
return ToolResult(
|
|
314
|
+
success=False, output="", error=f"Download timed out after {timeout} seconds"
|
|
315
|
+
)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
return ToolResult(success=False, output="", error=f"Download error: {str(e)}")
|
|
318
|
+
|
|
319
|
+
def _sync_download(self, url: str, file_path: Path, timeout: int) -> Dict[str, Any]:
|
|
320
|
+
"""Synchronous download implementation."""
|
|
321
|
+
try:
|
|
322
|
+
req = urllib.request.Request(url)
|
|
323
|
+
req.add_header("User-Agent", self.USER_AGENT)
|
|
324
|
+
|
|
325
|
+
ctx = ssl.create_default_context()
|
|
326
|
+
|
|
327
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
328
|
+
|
|
329
|
+
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as response:
|
|
330
|
+
with open(file_path, "wb") as f:
|
|
331
|
+
total = 0
|
|
332
|
+
while True:
|
|
333
|
+
chunk = response.read(8192)
|
|
334
|
+
if not chunk:
|
|
335
|
+
break
|
|
336
|
+
|
|
337
|
+
total += len(chunk)
|
|
338
|
+
if total > self.MAX_SIZE:
|
|
339
|
+
return {"error": f"File exceeds maximum size of {self.MAX_SIZE} bytes"}
|
|
340
|
+
|
|
341
|
+
f.write(chunk)
|
|
342
|
+
|
|
343
|
+
return {"size": total}
|
|
344
|
+
|
|
345
|
+
except urllib.error.HTTPError as e:
|
|
346
|
+
return {"error": f"HTTP {e.code}: {e.reason}"}
|
|
347
|
+
except urllib.error.URLError as e:
|
|
348
|
+
return {"error": f"URL Error: {str(e.reason)}"}
|
|
349
|
+
except Exception as e:
|
|
350
|
+
return {"error": str(e)}
|