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,323 @@
1
+ """
2
+ Error handling utilities for SuperQode OSS
3
+
4
+ Provides robust error handling for common edge cases and failure modes.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import logging
10
+ from typing import Optional, Dict, Any, Callable
11
+ from pathlib import Path
12
+ from functools import wraps
13
+ import asyncio
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class SuperQodeError(Exception):
19
+ """Base exception for SuperQode errors."""
20
+
21
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
22
+ super().__init__(message)
23
+ self.message = message
24
+ self.details = details or {}
25
+
26
+
27
+ class ConfigurationError(SuperQodeError):
28
+ """Configuration-related errors."""
29
+
30
+ pass
31
+
32
+
33
+ class DependencyError(SuperQodeError):
34
+ """Missing dependency errors."""
35
+
36
+ pass
37
+
38
+
39
+ class NetworkError(SuperQodeError):
40
+ """Network connectivity errors."""
41
+
42
+ pass
43
+
44
+
45
+ class TimeoutError(SuperQodeError):
46
+ """Timeout-related errors."""
47
+
48
+ pass
49
+
50
+
51
+ class ResourceError(SuperQodeError):
52
+ """Resource exhaustion errors."""
53
+
54
+ pass
55
+
56
+
57
+ def handle_errors(fallback_message: str = "An unexpected error occurred"):
58
+ """Decorator to handle common errors gracefully."""
59
+
60
+ def decorator(func):
61
+ @wraps(func)
62
+ async def async_wrapper(*args, **kwargs):
63
+ try:
64
+ return await func(*args, **kwargs)
65
+ except Exception as e:
66
+ return handle_error(e, fallback_message, func.__name__)
67
+
68
+ @wraps(func)
69
+ def sync_wrapper(*args, **kwargs):
70
+ try:
71
+ return func(*args, **kwargs)
72
+ except Exception as e:
73
+ return handle_error(e, fallback_message, func.__name__)
74
+
75
+ if asyncio.iscoroutinefunction(func):
76
+ return async_wrapper
77
+ return sync_wrapper
78
+
79
+ return decorator
80
+
81
+
82
+ def handle_error(error: Exception, fallback_message: str, context: str = "") -> Optional[Any]:
83
+ """Handle an error with appropriate logging and user-friendly messages."""
84
+
85
+ error_type = type(error).__name__
86
+
87
+ # Log the full error for debugging
88
+ logger.error(f"Error in {context}: {error_type}: {error}", exc_info=True)
89
+
90
+ # Handle specific error types with user-friendly messages
91
+ if isinstance(error, (OSError, PermissionError)):
92
+ if "permission denied" in str(error).lower():
93
+ print(f"❌ Permission denied: {error}")
94
+ print("💡 Try running with appropriate permissions or check file access.")
95
+ return None
96
+ elif "no space left on device" in str(error).lower():
97
+ print(f"❌ Disk full: {error}")
98
+ print("💡 Free up disk space and try again.")
99
+ return None
100
+ else:
101
+ print(f"❌ System error: {error}")
102
+
103
+ elif isinstance(error, ImportError):
104
+ if "opencode" in str(error).lower():
105
+ print("❌ OpenCode not found. Install with: npm i -g opencode-ai")
106
+ print("💡 OpenCode is required for AI agent analysis.")
107
+ else:
108
+ print(f"❌ Missing dependency: {error}")
109
+ return None
110
+
111
+ elif isinstance(error, asyncio.TimeoutError):
112
+ print(f"⏰ Operation timed out: {error}")
113
+ print("💡 Try increasing timeout or checking network connectivity.")
114
+ return None
115
+
116
+ elif isinstance(error, ConnectionError):
117
+ print(f"🌐 Network error: {error}")
118
+ print("💡 Check your internet connection and try again.")
119
+ return None
120
+
121
+ elif isinstance(error, MemoryError):
122
+ print(f"💾 Out of memory: {error}")
123
+ print("💡 Try closing other applications or reducing workload.")
124
+ return None
125
+
126
+ elif isinstance(error, FileNotFoundError):
127
+ if "opencode" in str(error).lower():
128
+ print("❌ OpenCode command not found.")
129
+ print("💡 Install OpenCode: npm i -g opencode-ai")
130
+ else:
131
+ print(f"❌ File not found: {error}")
132
+ return None
133
+
134
+ else:
135
+ # Generic error handling
136
+ print(f"❌ {fallback_message}: {error}")
137
+ if context:
138
+ print(f" Context: {context}")
139
+
140
+ return None
141
+
142
+
143
+ def check_dependencies():
144
+ """Check for required dependencies and provide helpful error messages."""
145
+
146
+ issues = []
147
+
148
+ # Check for Python version
149
+ if sys.version_info < (3, 8):
150
+ issues.append("Python 3.8+ required (current: {}.{}.{})".format(*sys.version_info[:3]))
151
+
152
+ # Check for Node.js and npm (for OpenCode)
153
+ try:
154
+ import subprocess
155
+
156
+ result = subprocess.run(["node", "--version"], capture_output=True, text=True, timeout=5)
157
+ if result.returncode != 0:
158
+ issues.append("Node.js not found or not working")
159
+ except (subprocess.TimeoutExpired, FileNotFoundError):
160
+ issues.append("Node.js not found - required for OpenCode")
161
+
162
+ try:
163
+ result = subprocess.run(["npm", "--version"], capture_output=True, text=True, timeout=5)
164
+ if result.returncode != 0:
165
+ issues.append("npm not found or not working")
166
+ except (subprocess.TimeoutExpired, FileNotFoundError):
167
+ issues.append("npm not found - required for OpenCode")
168
+
169
+ # Check for OpenCode
170
+ if not os.path.exists("/usr/local/bin/opencode") and not os.path.exists("/usr/bin/opencode"):
171
+ try:
172
+ result = subprocess.run(
173
+ ["which", "opencode"], capture_output=True, text=True, timeout=5
174
+ )
175
+ if result.returncode != 0:
176
+ issues.append("OpenCode not installed - install with: npm i -g opencode-ai")
177
+ except (subprocess.TimeoutExpired, FileNotFoundError):
178
+ issues.append("OpenCode not found - install with: npm i -g opencode-ai")
179
+
180
+ if issues:
181
+ print("⚠️ Dependency Issues Found:")
182
+ for issue in issues:
183
+ print(f" • {issue}")
184
+ print("\n🔧 Fix these issues before running SuperQode QE features.")
185
+
186
+ return len(issues) == 0
187
+
188
+
189
+ def validate_project_structure(project_root: Path) -> Dict[str, Any]:
190
+ """Validate project structure and return issues found."""
191
+
192
+ issues = {"warnings": [], "errors": [], "missing_files": [], "large_files": []}
193
+
194
+ # Check for common project files
195
+ common_files = ["package.json", "requirements.txt", "pyproject.toml", "Pipfile", "yarn.lock"]
196
+ has_project_file = any((project_root / f).exists() for f in common_files)
197
+
198
+ if not has_project_file:
199
+ issues["warnings"].append(
200
+ "No standard project file found (package.json, requirements.txt, etc.)"
201
+ )
202
+
203
+ # Check for large files that might cause issues
204
+ large_files = []
205
+ total_size = 0
206
+
207
+ try:
208
+ for file_path in project_root.rglob("*"):
209
+ if file_path.is_file() and not any(part.startswith(".") for part in file_path.parts):
210
+ try:
211
+ size = file_path.stat().st_size
212
+ total_size += size
213
+
214
+ # Flag files over 50MB
215
+ if size > 50 * 1024 * 1024:
216
+ large_files.append(f"{file_path.name} ({size / (1024 * 1024):.1f}MB)")
217
+
218
+ # Flag files over 10MB as warnings
219
+ elif size > 10 * 1024 * 1024:
220
+ issues["warnings"].append(
221
+ f"Large file: {file_path.name} ({size / (1024 * 1024):.1f}MB)"
222
+ )
223
+
224
+ except (OSError, PermissionError):
225
+ continue
226
+
227
+ if large_files:
228
+ issues["errors"].extend([f"File too large for analysis: {f}" for f in large_files])
229
+
230
+ # Check total project size (warn over 500MB)
231
+ if total_size > 500 * 1024 * 1024:
232
+ issues["warnings"].append(".1f")
233
+
234
+ except Exception as e:
235
+ issues["warnings"].append(f"Could not analyze project structure: {e}")
236
+
237
+ return issues
238
+
239
+
240
+ def create_fallback_result(operation: str, error: Exception) -> Dict[str, Any]:
241
+ """Create a fallback result when an operation fails."""
242
+
243
+ return {
244
+ "success": False,
245
+ "operation": operation,
246
+ "error": str(error),
247
+ "error_type": type(error).__name__,
248
+ "fallback": True,
249
+ "message": f"Operation '{operation}' failed, using fallback mode",
250
+ }
251
+
252
+
253
+ def safe_file_operation(operation_name: str):
254
+ """Decorator for safe file operations."""
255
+
256
+ def decorator(func):
257
+ @wraps(func)
258
+ def wrapper(*args, **kwargs):
259
+ try:
260
+ return func(*args, **kwargs)
261
+ except (OSError, PermissionError) as e:
262
+ logger.warning(f"File operation '{operation_name}' failed: {e}")
263
+ return create_fallback_result(operation_name, e)
264
+ except Exception as e:
265
+ logger.error(f"Unexpected error in '{operation_name}': {e}")
266
+ return create_fallback_result(operation_name, e)
267
+
268
+ return wrapper
269
+
270
+ return decorator
271
+
272
+
273
+ def safe_network_operation(operation_name: str, timeout: int = 30):
274
+ """Decorator for safe network operations."""
275
+
276
+ def decorator(func):
277
+ @wraps(func)
278
+ async def async_wrapper(*args, **kwargs):
279
+ try:
280
+ return await asyncio.wait_for(func(*args, **kwargs), timeout=timeout)
281
+ except asyncio.TimeoutError:
282
+ logger.warning(f"Network operation '{operation_name}' timed out")
283
+ return create_fallback_result(
284
+ operation_name, asyncio.TimeoutError("Operation timed out")
285
+ )
286
+ except Exception as e:
287
+ logger.error(f"Network operation '{operation_name}' failed: {e}")
288
+ return create_fallback_result(operation_name, e)
289
+
290
+ return async_wrapper
291
+
292
+ return decorator
293
+
294
+
295
+ # Global error recovery strategies
296
+ def attempt_recovery(func: Callable, max_retries: int = 3, backoff_factor: float = 1.5):
297
+ """Attempt to recover from transient failures."""
298
+
299
+ import time
300
+
301
+ @wraps(func)
302
+ async def async_wrapper(*args, **kwargs):
303
+ last_error = None
304
+
305
+ for attempt in range(max_retries):
306
+ try:
307
+ return await func(*args, **kwargs)
308
+ except (ConnectionError, OSError) as e:
309
+ last_error = e
310
+ if attempt < max_retries - 1:
311
+ delay = backoff_factor**attempt
312
+ logger.info(f"Attempt {attempt + 1} failed, retrying in {delay:.1f}s: {e}")
313
+ await asyncio.sleep(delay)
314
+ else:
315
+ logger.error(f"All {max_retries} attempts failed: {e}")
316
+ except Exception as e:
317
+ # Don't retry for non-transient errors
318
+ raise e
319
+
320
+ # If we get here, all retries failed
321
+ raise last_error
322
+
323
+ return async_wrapper
@@ -0,0 +1,257 @@
1
+ """Fuzzy search utilities with LRU caching for fast completion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from functools import lru_cache
7
+ from typing import NamedTuple
8
+
9
+
10
+ class FuzzyMatch(NamedTuple):
11
+ """A fuzzy match result with score and match positions."""
12
+
13
+ text: str
14
+ score: float
15
+ positions: list[int] # Character positions that matched
16
+
17
+
18
+ class FuzzySearch:
19
+ """
20
+ Fast fuzzy search with LRU caching.
21
+
22
+ Inspired by fzf/fuzzysort algorithms with:
23
+ - Bonus for matches at word boundaries
24
+ - Bonus for contiguous matches
25
+ - Case-insensitive matching with case-match bonus
26
+ """
27
+
28
+ def __init__(self, max_cache_size: int = 1024):
29
+ """Initialize with cache size."""
30
+ self._cache_size = max_cache_size
31
+ # Create cached version of core search
32
+ self._cached_score = lru_cache(maxsize=max_cache_size)(self._compute_score)
33
+
34
+ def _compute_score(self, query: str, text: str) -> tuple[float, tuple[int, ...]]:
35
+ """
36
+ Compute fuzzy match score between query and text.
37
+
38
+ Returns (score, positions) where higher score = better match.
39
+ """
40
+ if not query:
41
+ return (0.0, ())
42
+
43
+ query_lower = query.lower()
44
+ text_lower = text.lower()
45
+
46
+ # Quick rejection: all query chars must be in text
47
+ for char in query_lower:
48
+ if char not in text_lower:
49
+ return (-1.0, ())
50
+
51
+ # Find best match using greedy algorithm
52
+ positions: list[int] = []
53
+ score = 0.0
54
+ query_idx = 0
55
+ prev_match_idx = -2 # For contiguity bonus
56
+
57
+ # Word boundary detection
58
+ word_boundaries = {0} # Start is always a boundary
59
+ for i, char in enumerate(text):
60
+ if i > 0:
61
+ prev_char = text[i - 1]
62
+ # Boundary after: space, underscore, hyphen, slash, dot
63
+ # Or transition from lowercase to uppercase (camelCase)
64
+ if prev_char in " _-/." or (prev_char.islower() and char.isupper()):
65
+ word_boundaries.add(i)
66
+
67
+ for text_idx, char in enumerate(text_lower):
68
+ if query_idx >= len(query_lower):
69
+ break
70
+
71
+ if char == query_lower[query_idx]:
72
+ positions.append(text_idx)
73
+
74
+ # Base score for match
75
+ match_score = 1.0
76
+
77
+ # Bonus for word boundary match
78
+ if text_idx in word_boundaries:
79
+ match_score += 2.0
80
+
81
+ # Bonus for exact case match
82
+ if text[text_idx] == query[query_idx]:
83
+ match_score += 0.5
84
+
85
+ # Bonus for contiguous matches
86
+ if text_idx == prev_match_idx + 1:
87
+ match_score += 1.5
88
+
89
+ # Bonus for matching at start
90
+ if text_idx == 0:
91
+ match_score += 3.0
92
+
93
+ score += match_score
94
+ prev_match_idx = text_idx
95
+ query_idx += 1
96
+
97
+ # All query characters must be matched
98
+ if query_idx < len(query_lower):
99
+ return (-1.0, ())
100
+
101
+ # Normalize score by query length and penalize by text length
102
+ # Shorter texts with same matches are preferred
103
+ normalized_score = score / len(query) - (len(text) * 0.01)
104
+
105
+ return (normalized_score, tuple(positions))
106
+
107
+ def search(
108
+ self,
109
+ query: str,
110
+ items: list[str],
111
+ max_results: int = 10,
112
+ threshold: float = 0.0,
113
+ ) -> list[FuzzyMatch]:
114
+ """
115
+ Search items for fuzzy matches to query.
116
+
117
+ Args:
118
+ query: The search query
119
+ items: List of strings to search
120
+ max_results: Maximum number of results to return
121
+ threshold: Minimum score threshold (default 0 = any match)
122
+
123
+ Returns:
124
+ List of FuzzyMatch objects sorted by score (best first)
125
+ """
126
+ if not query:
127
+ # Return first max_results items with score 0
128
+ return [FuzzyMatch(text=item, score=0.0, positions=[]) for item in items[:max_results]]
129
+
130
+ results: list[FuzzyMatch] = []
131
+
132
+ for item in items:
133
+ score, positions = self._cached_score(query, item)
134
+ if score >= threshold:
135
+ results.append(FuzzyMatch(text=item, score=score, positions=list(positions)))
136
+
137
+ # Sort by score descending
138
+ results.sort(key=lambda x: x.score, reverse=True)
139
+
140
+ return results[:max_results]
141
+
142
+ def search_with_data(
143
+ self,
144
+ query: str,
145
+ items: list[tuple[str, any]],
146
+ max_results: int = 10,
147
+ threshold: float = 0.0,
148
+ ) -> list[tuple[FuzzyMatch, any]]:
149
+ """
150
+ Search items with associated data.
151
+
152
+ Args:
153
+ query: The search query
154
+ items: List of (searchable_text, data) tuples
155
+ max_results: Maximum number of results
156
+ threshold: Minimum score threshold
157
+
158
+ Returns:
159
+ List of (FuzzyMatch, data) tuples sorted by score
160
+ """
161
+ if not query:
162
+ return [
163
+ (FuzzyMatch(text=text, score=0.0, positions=[]), data)
164
+ for text, data in items[:max_results]
165
+ ]
166
+
167
+ results: list[tuple[FuzzyMatch, any]] = []
168
+
169
+ for text, data in items:
170
+ score, positions = self._cached_score(query, text)
171
+ if score >= threshold:
172
+ results.append(
173
+ (FuzzyMatch(text=text, score=score, positions=list(positions)), data)
174
+ )
175
+
176
+ results.sort(key=lambda x: x[0].score, reverse=True)
177
+
178
+ return results[:max_results]
179
+
180
+ def highlight_match(
181
+ self,
182
+ text: str,
183
+ positions: list[int],
184
+ highlight_start: str = "[bold cyan]",
185
+ highlight_end: str = "[/bold cyan]",
186
+ ) -> str:
187
+ """
188
+ Apply Rich markup to highlight matched positions.
189
+
190
+ Args:
191
+ text: Original text
192
+ positions: List of matched character positions
193
+ highlight_start: Rich markup to start highlight
194
+ highlight_end: Rich markup to end highlight
195
+
196
+ Returns:
197
+ Text with Rich markup applied to matched characters
198
+ """
199
+ if not positions:
200
+ return text
201
+
202
+ result = []
203
+ pos_set = set(positions)
204
+ in_highlight = False
205
+
206
+ for i, char in enumerate(text):
207
+ if i in pos_set:
208
+ if not in_highlight:
209
+ result.append(highlight_start)
210
+ in_highlight = True
211
+ result.append(char)
212
+ else:
213
+ if in_highlight:
214
+ result.append(highlight_end)
215
+ in_highlight = False
216
+ result.append(char)
217
+
218
+ if in_highlight:
219
+ result.append(highlight_end)
220
+
221
+ return "".join(result)
222
+
223
+ def clear_cache(self) -> None:
224
+ """Clear the search cache."""
225
+ self._cached_score.cache_clear()
226
+
227
+
228
+ class PathFuzzySearch(FuzzySearch):
229
+ """
230
+ Specialized fuzzy search for file paths.
231
+
232
+ Gives extra weight to:
233
+ - Filename matches (last segment)
234
+ - Matches after path separators
235
+ """
236
+
237
+ def _compute_score(self, query: str, text: str) -> tuple[float, tuple[int, ...]]:
238
+ """Compute path-aware fuzzy match score."""
239
+ base_score, positions = super()._compute_score(query, text)
240
+
241
+ if base_score < 0:
242
+ return (base_score, positions)
243
+
244
+ # Bonus for matches in filename (after last /)
245
+ last_sep = text.rfind("/")
246
+ if last_sep >= 0:
247
+ filename_positions = [p for p in positions if p > last_sep]
248
+ if filename_positions:
249
+ # Significant bonus for filename matches
250
+ base_score += len(filename_positions) * 2.0
251
+
252
+ return (base_score, positions)
253
+
254
+
255
+ # Global instances for convenience
256
+ fuzzy_search = FuzzySearch()
257
+ path_fuzzy_search = PathFuzzySearch()