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,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
|
superqode/utils/fuzzy.py
ADDED
|
@@ -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()
|