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.
Files changed (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,639 @@
1
+ """
2
+ Web Tools - Search and Fetch Web Content.
3
+
4
+ Provides web search and content fetching capabilities for agents:
5
+ - Web search with multiple provider support
6
+ - URL content fetching with summarization
7
+ - HTML to markdown conversion
8
+
9
+ Features:
10
+ - DuckDuckGo search (no API key required)
11
+ - Optional Tavily/SerpAPI integration
12
+ - Content extraction and summarization
13
+ - Configurable result limits
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import json
20
+ import re
21
+ import ssl
22
+ import urllib.request
23
+ import urllib.error
24
+ import urllib.parse
25
+ from html.parser import HTMLParser
26
+ from typing import Any, Dict, List, Optional
27
+ from dataclasses import dataclass
28
+
29
+ from .base import Tool, ToolResult, ToolContext
30
+
31
+
32
+ # ============================================================================
33
+ # HTML Processing Utilities
34
+ # ============================================================================
35
+
36
+
37
+ class HTMLToMarkdown(HTMLParser):
38
+ """Convert HTML to Markdown format."""
39
+
40
+ def __init__(self):
41
+ super().__init__()
42
+ self._output: List[str] = []
43
+ self._skip_tags = {"script", "style", "head", "meta", "link", "noscript"}
44
+ self._in_skip = False
45
+ self._tag_stack: List[str] = []
46
+ self._list_depth = 0
47
+ self._in_code = False
48
+ self._href = ""
49
+
50
+ def handle_starttag(self, tag: str, attrs: List[tuple]):
51
+ tag = tag.lower()
52
+ self._tag_stack.append(tag)
53
+
54
+ if tag in self._skip_tags:
55
+ self._in_skip = True
56
+ return
57
+
58
+ attrs_dict = dict(attrs)
59
+
60
+ if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
61
+ level = int(tag[1])
62
+ self._output.append("\n" + "#" * level + " ")
63
+ elif tag == "p":
64
+ self._output.append("\n\n")
65
+ elif tag == "br":
66
+ self._output.append("\n")
67
+ elif tag in ("strong", "b"):
68
+ self._output.append("**")
69
+ elif tag in ("em", "i"):
70
+ self._output.append("*")
71
+ elif tag == "a":
72
+ self._href = attrs_dict.get("href", "")
73
+ self._output.append("[")
74
+ elif tag == "code":
75
+ if self._in_code:
76
+ return
77
+ self._output.append("`")
78
+ self._in_code = True
79
+ elif tag == "pre":
80
+ self._output.append("\n```\n")
81
+ self._in_code = True
82
+ elif tag == "ul":
83
+ self._list_depth += 1
84
+ self._output.append("\n")
85
+ elif tag == "ol":
86
+ self._list_depth += 1
87
+ self._output.append("\n")
88
+ elif tag == "li":
89
+ indent = " " * (self._list_depth - 1)
90
+ self._output.append(f"{indent}- ")
91
+ elif tag == "blockquote":
92
+ self._output.append("\n> ")
93
+ elif tag == "hr":
94
+ self._output.append("\n---\n")
95
+
96
+ def handle_endtag(self, tag: str):
97
+ tag = tag.lower()
98
+
99
+ if self._tag_stack and self._tag_stack[-1] == tag:
100
+ self._tag_stack.pop()
101
+
102
+ if tag in self._skip_tags:
103
+ self._in_skip = False
104
+ return
105
+
106
+ if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
107
+ self._output.append("\n")
108
+ elif tag == "p":
109
+ self._output.append("\n")
110
+ elif tag in ("strong", "b"):
111
+ self._output.append("**")
112
+ elif tag in ("em", "i"):
113
+ self._output.append("*")
114
+ elif tag == "a":
115
+ if self._href:
116
+ self._output.append(f"]({self._href})")
117
+ else:
118
+ self._output.append("]")
119
+ self._href = ""
120
+ elif tag == "code":
121
+ self._output.append("`")
122
+ self._in_code = False
123
+ elif tag == "pre":
124
+ self._output.append("\n```\n")
125
+ self._in_code = False
126
+ elif tag == "ul" or tag == "ol":
127
+ self._list_depth = max(0, self._list_depth - 1)
128
+ self._output.append("\n")
129
+ elif tag == "li":
130
+ self._output.append("\n")
131
+
132
+ def handle_data(self, data: str):
133
+ if self._in_skip:
134
+ return
135
+ text = data.strip() if not self._in_code else data
136
+ if text:
137
+ self._output.append(text)
138
+
139
+ def get_markdown(self) -> str:
140
+ """Get the converted markdown."""
141
+ result = "".join(self._output)
142
+ # Clean up extra whitespace
143
+ result = re.sub(r"\n{3,}", "\n\n", result)
144
+ return result.strip()
145
+
146
+
147
+ class TextExtractor(HTMLParser):
148
+ """Extract plain text from HTML."""
149
+
150
+ def __init__(self):
151
+ super().__init__()
152
+ self._text: List[str] = []
153
+ self._skip_tags = {"script", "style", "head", "meta", "link", "noscript"}
154
+ self._in_skip = False
155
+
156
+ def handle_starttag(self, tag: str, attrs: List[tuple]):
157
+ if tag.lower() in self._skip_tags:
158
+ self._in_skip = True
159
+
160
+ def handle_endtag(self, tag: str):
161
+ if tag.lower() in self._skip_tags:
162
+ self._in_skip = False
163
+
164
+ def handle_data(self, data: str):
165
+ if not self._in_skip:
166
+ text = data.strip()
167
+ if text:
168
+ self._text.append(text)
169
+
170
+ def get_text(self) -> str:
171
+ return "\n".join(self._text)
172
+
173
+
174
+ # ============================================================================
175
+ # Web Search Tool
176
+ # ============================================================================
177
+
178
+
179
+ @dataclass
180
+ class SearchResult:
181
+ """A search result."""
182
+
183
+ title: str
184
+ url: str
185
+ snippet: str
186
+
187
+
188
+ class WebSearchTool(Tool):
189
+ """
190
+ Search the web for information.
191
+
192
+ Uses DuckDuckGo by default (no API key required).
193
+ Can be configured to use Tavily or SerpAPI with API keys.
194
+
195
+ Features:
196
+ - Multiple search providers
197
+ - Configurable result count
198
+ - Search type options (fast, deep)
199
+ """
200
+
201
+ USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
202
+ DEFAULT_TIMEOUT = 15
203
+ MAX_RESULTS = 10
204
+
205
+ @property
206
+ def name(self) -> str:
207
+ return "web_search"
208
+
209
+ @property
210
+ def description(self) -> str:
211
+ return """Search the web and return relevant results.
212
+
213
+ Returns a list of search results with titles, URLs, and snippets.
214
+ Useful for finding documentation, examples, recent information, etc."""
215
+
216
+ @property
217
+ def parameters(self) -> Dict[str, Any]:
218
+ return {
219
+ "type": "object",
220
+ "properties": {
221
+ "query": {"type": "string", "description": "Search query"},
222
+ "num_results": {
223
+ "type": "integer",
224
+ "description": "Number of results to return (default: 5, max: 10)",
225
+ },
226
+ "search_type": {
227
+ "type": "string",
228
+ "enum": ["auto", "fast", "deep"],
229
+ "description": "Search type: fast (quick results), deep (more comprehensive)",
230
+ },
231
+ },
232
+ "required": ["query"],
233
+ }
234
+
235
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
236
+ query = args.get("query", "")
237
+ num_results = min(args.get("num_results", 5), self.MAX_RESULTS)
238
+ search_type = args.get("search_type", "auto")
239
+
240
+ if not query:
241
+ return ToolResult(success=False, output="", error="Search query is required")
242
+
243
+ try:
244
+ # Try DuckDuckGo first (no API key needed)
245
+ results = await self._search_duckduckgo(query, num_results)
246
+
247
+ if not results:
248
+ return ToolResult(
249
+ success=True,
250
+ output=f"No results found for: {query}",
251
+ metadata={"query": query, "count": 0},
252
+ )
253
+
254
+ # Format results
255
+ output_lines = [f"Search results for: {query}\n"]
256
+
257
+ for i, result in enumerate(results, 1):
258
+ output_lines.append(f"{i}. {result.title}")
259
+ output_lines.append(f" URL: {result.url}")
260
+ if result.snippet:
261
+ # Truncate long snippets
262
+ snippet = (
263
+ result.snippet[:200] + "..."
264
+ if len(result.snippet) > 200
265
+ else result.snippet
266
+ )
267
+ output_lines.append(f" {snippet}")
268
+ output_lines.append("")
269
+
270
+ return ToolResult(
271
+ success=True,
272
+ output="\n".join(output_lines),
273
+ metadata={"query": query, "count": len(results), "search_type": search_type},
274
+ )
275
+
276
+ except Exception as e:
277
+ return ToolResult(success=False, output="", error=f"Search failed: {str(e)}")
278
+
279
+ async def _search_duckduckgo(self, query: str, num_results: int) -> List[SearchResult]:
280
+ """Search using DuckDuckGo HTML."""
281
+ loop = asyncio.get_event_loop()
282
+ return await loop.run_in_executor(
283
+ None, lambda: self._sync_search_duckduckgo(query, num_results)
284
+ )
285
+
286
+ def _sync_search_duckduckgo(self, query: str, num_results: int) -> List[SearchResult]:
287
+ """Synchronous DuckDuckGo search implementation."""
288
+ try:
289
+ # Use DuckDuckGo HTML search
290
+ encoded_query = urllib.parse.quote_plus(query)
291
+ url = f"https://html.duckduckgo.com/html/?q={encoded_query}"
292
+
293
+ req = urllib.request.Request(url)
294
+ req.add_header("User-Agent", self.USER_AGENT)
295
+
296
+ ctx = ssl.create_default_context()
297
+
298
+ with urllib.request.urlopen(req, timeout=self.DEFAULT_TIMEOUT, context=ctx) as response:
299
+ html = response.read().decode("utf-8", errors="replace")
300
+
301
+ # Parse results
302
+ results = self._parse_ddg_results(html, num_results)
303
+ return results
304
+
305
+ except Exception as e:
306
+ # Fallback to instant answer API
307
+ try:
308
+ return self._search_ddg_instant(query, num_results)
309
+ except Exception:
310
+ raise e
311
+
312
+ def _parse_ddg_results(self, html: str, num_results: int) -> List[SearchResult]:
313
+ """Parse DuckDuckGo HTML results."""
314
+ results = []
315
+
316
+ # Find result blocks
317
+ result_pattern = re.compile(
318
+ r'<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)</a>', re.DOTALL
319
+ )
320
+
321
+ snippet_pattern = re.compile(r'<a[^>]*class="result__snippet"[^>]*>([^<]*)</a>', re.DOTALL)
322
+
323
+ # Find all result links
324
+ link_matches = result_pattern.findall(html)
325
+ snippet_matches = snippet_pattern.findall(html)
326
+
327
+ for i, (url, title) in enumerate(link_matches[:num_results]):
328
+ # Clean URL (DuckDuckGo uses redirect URLs)
329
+ if "uddg=" in url:
330
+ try:
331
+ parsed = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
332
+ url = parsed.get("uddg", [url])[0]
333
+ except Exception:
334
+ pass
335
+
336
+ # Get snippet if available
337
+ snippet = snippet_matches[i] if i < len(snippet_matches) else ""
338
+
339
+ # Clean HTML entities
340
+ title = self._clean_html_entities(title)
341
+ snippet = self._clean_html_entities(snippet)
342
+
343
+ results.append(SearchResult(title=title.strip(), url=url, snippet=snippet.strip()))
344
+
345
+ return results
346
+
347
+ def _search_ddg_instant(self, query: str, num_results: int) -> List[SearchResult]:
348
+ """Search using DuckDuckGo instant answer API."""
349
+ encoded_query = urllib.parse.quote_plus(query)
350
+ url = f"https://api.duckduckgo.com/?q={encoded_query}&format=json&no_redirect=1"
351
+
352
+ req = urllib.request.Request(url)
353
+ req.add_header("User-Agent", self.USER_AGENT)
354
+
355
+ ctx = ssl.create_default_context()
356
+
357
+ with urllib.request.urlopen(req, timeout=self.DEFAULT_TIMEOUT, context=ctx) as response:
358
+ data = json.loads(response.read().decode("utf-8"))
359
+
360
+ results = []
361
+
362
+ # Abstract result
363
+ if data.get("AbstractURL") and data.get("AbstractText"):
364
+ results.append(
365
+ SearchResult(
366
+ title=data.get("Heading", query),
367
+ url=data["AbstractURL"],
368
+ snippet=data["AbstractText"][:200],
369
+ )
370
+ )
371
+
372
+ # Related topics
373
+ for topic in data.get("RelatedTopics", [])[: num_results - len(results)]:
374
+ if isinstance(topic, dict) and topic.get("FirstURL"):
375
+ results.append(
376
+ SearchResult(
377
+ title=topic.get("Text", "")[:100],
378
+ url=topic["FirstURL"],
379
+ snippet=topic.get("Text", "")[:200],
380
+ )
381
+ )
382
+
383
+ return results
384
+
385
+ def _clean_html_entities(self, text: str) -> str:
386
+ """Clean HTML entities from text."""
387
+ import html
388
+
389
+ return html.unescape(text)
390
+
391
+
392
+ # ============================================================================
393
+ # Web Fetch Tool (Enhanced)
394
+ # ============================================================================
395
+
396
+
397
+ class WebFetchTool(Tool):
398
+ """
399
+ Fetch and analyze web content.
400
+
401
+ Features:
402
+ - HTML to markdown conversion
403
+ - Text extraction
404
+ - Optional summarization
405
+ - Configurable output format
406
+ """
407
+
408
+ USER_AGENT = "SuperQode/1.0 (AI Coding Assistant)"
409
+ DEFAULT_TIMEOUT = 30
410
+ MAX_SIZE = 2 * 1024 * 1024 # 2MB
411
+
412
+ @property
413
+ def name(self) -> str:
414
+ return "web_fetch"
415
+
416
+ @property
417
+ def description(self) -> str:
418
+ return """Fetch content from a URL and optionally process it.
419
+
420
+ Supports:
421
+ - HTML pages (converts to markdown or extracts text)
422
+ - JSON APIs (formatted output)
423
+ - Plain text content
424
+
425
+ Useful for reading documentation, API responses, web pages, etc."""
426
+
427
+ @property
428
+ def parameters(self) -> Dict[str, Any]:
429
+ return {
430
+ "type": "object",
431
+ "properties": {
432
+ "url": {"type": "string", "description": "URL to fetch (http or https)"},
433
+ "format": {
434
+ "type": "string",
435
+ "enum": ["auto", "markdown", "text", "json", "raw"],
436
+ "description": "Output format: markdown (HTML to MD), text (plain text), json, raw",
437
+ },
438
+ "extract_main": {
439
+ "type": "boolean",
440
+ "description": "Try to extract main content only (skip navigation, ads)",
441
+ },
442
+ "max_length": {
443
+ "type": "integer",
444
+ "description": "Maximum content length in characters (default: 50000)",
445
+ },
446
+ "selector": {
447
+ "type": "string",
448
+ "description": "CSS-like selector to extract specific content (e.g., 'article', 'main')",
449
+ },
450
+ },
451
+ "required": ["url"],
452
+ }
453
+
454
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
455
+ url = args.get("url", "")
456
+ format_type = args.get("format", "auto")
457
+ extract_main = args.get("extract_main", True)
458
+ max_length = args.get("max_length", 50000)
459
+ selector = args.get("selector", "")
460
+
461
+ if not url:
462
+ return ToolResult(success=False, output="", error="URL is required")
463
+
464
+ if not url.startswith(("http://", "https://")):
465
+ return ToolResult(
466
+ success=False, output="", error="Only http:// and https:// URLs are supported"
467
+ )
468
+
469
+ try:
470
+ loop = asyncio.get_event_loop()
471
+ result = await asyncio.wait_for(
472
+ loop.run_in_executor(None, lambda: self._sync_fetch(url)),
473
+ timeout=self.DEFAULT_TIMEOUT + 5,
474
+ )
475
+
476
+ if result.get("error"):
477
+ return ToolResult(success=False, output="", error=result["error"])
478
+
479
+ content = result["content"]
480
+ content_type = result.get("content_type", "")
481
+
482
+ # Process content based on format
483
+ output = self._process_content(
484
+ content, content_type, format_type, extract_main, selector
485
+ )
486
+
487
+ # Truncate if needed
488
+ if len(output) > max_length:
489
+ output = output[:max_length] + f"\n\n[Content truncated at {max_length} characters]"
490
+
491
+ return ToolResult(
492
+ success=True,
493
+ output=output,
494
+ metadata={
495
+ "url": url,
496
+ "content_type": content_type,
497
+ "original_size": len(content),
498
+ "output_size": len(output),
499
+ "format": format_type,
500
+ },
501
+ )
502
+
503
+ except asyncio.TimeoutError:
504
+ return ToolResult(
505
+ success=False,
506
+ output="",
507
+ error=f"Request timed out after {self.DEFAULT_TIMEOUT} seconds",
508
+ )
509
+ except Exception as e:
510
+ return ToolResult(success=False, output="", error=f"Fetch error: {str(e)}")
511
+
512
+ def _sync_fetch(self, url: str) -> Dict[str, Any]:
513
+ """Synchronous fetch implementation."""
514
+ try:
515
+ req = urllib.request.Request(url)
516
+ req.add_header("User-Agent", self.USER_AGENT)
517
+ req.add_header(
518
+ "Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
519
+ )
520
+
521
+ ctx = ssl.create_default_context()
522
+
523
+ with urllib.request.urlopen(req, timeout=self.DEFAULT_TIMEOUT, context=ctx) as response:
524
+ content_type = response.headers.get("Content-Type", "")
525
+
526
+ # Read with size limit
527
+ content = response.read(self.MAX_SIZE)
528
+
529
+ # Check for truncation
530
+ extra = response.read(1)
531
+ truncated = bool(extra)
532
+
533
+ # Decode
534
+ charset = self._get_charset(content_type)
535
+ try:
536
+ text = content.decode(charset, errors="replace")
537
+ except (UnicodeDecodeError, LookupError):
538
+ text = content.decode("utf-8", errors="replace")
539
+
540
+ if truncated:
541
+ text += f"\n\n[Content truncated at {self.MAX_SIZE} bytes]"
542
+
543
+ return {"content": text, "content_type": content_type, "truncated": truncated}
544
+
545
+ except urllib.error.HTTPError as e:
546
+ return {"error": f"HTTP {e.code}: {e.reason}"}
547
+ except urllib.error.URLError as e:
548
+ return {"error": f"URL Error: {str(e.reason)}"}
549
+ except Exception as e:
550
+ return {"error": str(e)}
551
+
552
+ def _get_charset(self, content_type: str) -> str:
553
+ """Extract charset from Content-Type header."""
554
+ if not content_type:
555
+ return "utf-8"
556
+
557
+ match = re.search(r"charset=([^\s;]+)", content_type, re.I)
558
+ if match:
559
+ return match.group(1).strip("\"'")
560
+
561
+ return "utf-8"
562
+
563
+ def _process_content(
564
+ self, content: str, content_type: str, format_type: str, extract_main: bool, selector: str
565
+ ) -> str:
566
+ """Process content based on format type."""
567
+ # Auto-detect format
568
+ if format_type == "auto":
569
+ if "application/json" in content_type:
570
+ format_type = "json"
571
+ elif "text/html" in content_type:
572
+ format_type = "markdown"
573
+ else:
574
+ format_type = "raw"
575
+
576
+ if format_type == "json":
577
+ try:
578
+ data = json.loads(content)
579
+ return json.dumps(data, indent=2)
580
+ except json.JSONDecodeError:
581
+ return content
582
+
583
+ elif format_type == "markdown":
584
+ # Convert HTML to Markdown
585
+ try:
586
+ # Optionally extract main content first
587
+ if extract_main:
588
+ content = self._extract_main_content(content, selector)
589
+
590
+ parser = HTMLToMarkdown()
591
+ parser.feed(content)
592
+ return parser.get_markdown()
593
+ except Exception:
594
+ return content
595
+
596
+ elif format_type == "text":
597
+ # Extract plain text
598
+ try:
599
+ parser = TextExtractor()
600
+ parser.feed(content)
601
+ return parser.get_text()
602
+ except Exception:
603
+ return content
604
+
605
+ else: # raw
606
+ return content
607
+
608
+ def _extract_main_content(self, html: str, selector: str) -> str:
609
+ """Extract main content from HTML."""
610
+ # Simple extraction based on common patterns
611
+ # Priority: article, main, .content, .post, #content, body
612
+
613
+ patterns = [
614
+ (r"<article[^>]*>(.*?)</article>", "article"),
615
+ (r"<main[^>]*>(.*?)</main>", "main"),
616
+ (r'<div[^>]*class="[^"]*content[^"]*"[^>]*>(.*?)</div>', ".content"),
617
+ (r'<div[^>]*id="content"[^>]*>(.*?)</div>', "#content"),
618
+ ]
619
+
620
+ if selector:
621
+ # Custom selector (simplified)
622
+ if selector.startswith("."):
623
+ class_name = selector[1:]
624
+ patterns.insert(
625
+ 0, (rf'<[^>]*class="[^"]*{class_name}[^"]*"[^>]*>(.*?)</\w+>', selector)
626
+ )
627
+ elif selector.startswith("#"):
628
+ id_name = selector[1:]
629
+ patterns.insert(0, (rf'<[^>]*id="{id_name}"[^>]*>(.*?)</\w+>', selector))
630
+ else:
631
+ patterns.insert(0, (rf"<{selector}[^>]*>(.*?)</{selector}>", selector))
632
+
633
+ for pattern, name in patterns:
634
+ match = re.search(pattern, html, re.DOTALL | re.IGNORECASE)
635
+ if match:
636
+ return match.group(1)
637
+
638
+ # Return original if no main content found
639
+ return html