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,775 @@
1
+ """
2
+ SuperQode LSP Server - Language Server Protocol for QE Integration.
3
+
4
+ Provides IDE integration by exposing QE findings as LSP diagnostics.
5
+ Supports VSCode, Neovim, and other LSP-compatible editors.
6
+
7
+ Features:
8
+ - Real-time QIR findings as diagnostics
9
+ - Quick fixes from QE patches
10
+ - Code actions for findings
11
+ - Status updates during QE sessions
12
+
13
+ Usage:
14
+ superqode serve --lsp # Start LSP server (stdio)
15
+ superqode serve --lsp --port 9000 # Start LSP server (TCP)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import json
22
+ import logging
23
+ import sys
24
+ import threading
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import Any, Callable, Dict, List, Optional, Tuple
28
+ from enum import IntEnum
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class DiagnosticSeverity(IntEnum):
34
+ """LSP diagnostic severity levels."""
35
+
36
+ ERROR = 1
37
+ WARNING = 2
38
+ INFORMATION = 3
39
+ HINT = 4
40
+
41
+
42
+ class CodeActionKind:
43
+ """LSP code action kinds."""
44
+
45
+ QUICKFIX = "quickfix"
46
+ REFACTOR = "refactor"
47
+ SOURCE = "source"
48
+
49
+
50
+ @dataclass
51
+ class LSPPosition:
52
+ """Position in a text document (0-indexed)."""
53
+
54
+ line: int
55
+ character: int
56
+
57
+ def to_dict(self) -> dict:
58
+ return {"line": self.line, "character": self.character}
59
+
60
+ @classmethod
61
+ def from_dict(cls, data: dict) -> "LSPPosition":
62
+ return cls(line=data["line"], character=data["character"])
63
+
64
+
65
+ @dataclass
66
+ class LSPRange:
67
+ """Range in a text document."""
68
+
69
+ start: LSPPosition
70
+ end: LSPPosition
71
+
72
+ def to_dict(self) -> dict:
73
+ return {"start": self.start.to_dict(), "end": self.end.to_dict()}
74
+
75
+ @classmethod
76
+ def from_dict(cls, data: dict) -> "LSPRange":
77
+ return cls(
78
+ start=LSPPosition.from_dict(data["start"]),
79
+ end=LSPPosition.from_dict(data["end"]),
80
+ )
81
+
82
+
83
+ @dataclass
84
+ class LSPDiagnostic:
85
+ """A diagnostic (error, warning, etc.)."""
86
+
87
+ range: LSPRange
88
+ message: str
89
+ severity: DiagnosticSeverity = DiagnosticSeverity.WARNING
90
+ code: Optional[str] = None
91
+ source: str = "superqode"
92
+ data: Optional[Dict] = None # For code actions
93
+
94
+ def to_dict(self) -> dict:
95
+ result = {
96
+ "range": self.range.to_dict(),
97
+ "message": self.message,
98
+ "severity": self.severity.value,
99
+ "source": self.source,
100
+ }
101
+ if self.code:
102
+ result["code"] = self.code
103
+ if self.data:
104
+ result["data"] = self.data
105
+ return result
106
+
107
+
108
+ @dataclass
109
+ class TextEdit:
110
+ """A text edit operation."""
111
+
112
+ range: LSPRange
113
+ new_text: str
114
+
115
+ def to_dict(self) -> dict:
116
+ return {
117
+ "range": self.range.to_dict(),
118
+ "newText": self.new_text,
119
+ }
120
+
121
+
122
+ @dataclass
123
+ class CodeAction:
124
+ """A code action (quick fix, refactor, etc.)."""
125
+
126
+ title: str
127
+ kind: str
128
+ diagnostics: List[LSPDiagnostic] = field(default_factory=list)
129
+ edit: Optional[Dict] = None
130
+ command: Optional[Dict] = None
131
+ is_preferred: bool = False
132
+
133
+ def to_dict(self) -> dict:
134
+ result = {
135
+ "title": self.title,
136
+ "kind": self.kind,
137
+ }
138
+ if self.diagnostics:
139
+ result["diagnostics"] = [d.to_dict() for d in self.diagnostics]
140
+ if self.edit:
141
+ result["edit"] = self.edit
142
+ if self.command:
143
+ result["command"] = self.command
144
+ if self.is_preferred:
145
+ result["isPreferred"] = True
146
+ return result
147
+
148
+
149
+ # Severity mapping from QE to LSP
150
+ QE_TO_LSP_SEVERITY = {
151
+ "critical": DiagnosticSeverity.ERROR,
152
+ "high": DiagnosticSeverity.ERROR,
153
+ "medium": DiagnosticSeverity.WARNING,
154
+ "low": DiagnosticSeverity.INFORMATION,
155
+ "info": DiagnosticSeverity.HINT,
156
+ }
157
+
158
+
159
+ class SuperQodeLSPServer:
160
+ """
161
+ SuperQode Language Server Protocol Server.
162
+
163
+ Exposes QE findings as LSP diagnostics for IDE integration.
164
+ """
165
+
166
+ def __init__(
167
+ self,
168
+ project_root: Optional[Path] = None,
169
+ transport: str = "stdio",
170
+ port: int = 9000,
171
+ ):
172
+ self.project_root = project_root
173
+ self.transport = transport
174
+ self.port = port
175
+
176
+ # Server state
177
+ self._initialized = False
178
+ self._shutdown = False
179
+ self._request_id = 0
180
+
181
+ # Document tracking
182
+ self._open_documents: Dict[str, str] = {} # uri -> content
183
+
184
+ # QE findings
185
+ self._findings_by_file: Dict[str, List[Dict]] = {}
186
+ self._patches_by_file: Dict[str, List[Dict]] = {}
187
+
188
+ # Diagnostics cache
189
+ self._diagnostics: Dict[str, List[LSPDiagnostic]] = {}
190
+
191
+ # IO
192
+ self._stdin = None
193
+ self._stdout = None
194
+ self._reader_thread = None
195
+
196
+ # Callbacks
197
+ self._on_qe_request: Optional[Callable[[str], None]] = None
198
+
199
+ def _next_id(self) -> int:
200
+ """Get next request ID."""
201
+ self._request_id += 1
202
+ return self._request_id
203
+
204
+ def _uri_to_path(self, uri: str) -> Path:
205
+ """Convert file URI to path."""
206
+ if uri.startswith("file://"):
207
+ return Path(uri[7:])
208
+ return Path(uri)
209
+
210
+ def _path_to_uri(self, path: Path) -> str:
211
+ """Convert path to file URI."""
212
+ return f"file://{path.resolve()}"
213
+
214
+ # ================================================================
215
+ # Finding to Diagnostic Conversion
216
+ # ================================================================
217
+
218
+ def load_qir(self, qr_path: Path) -> None:
219
+ """Load findings from a QIR JSON file."""
220
+ try:
221
+ data = json.loads(qir_path.read_text())
222
+ findings = data.get("findings", [])
223
+ self._process_findings(findings)
224
+ logger.info(f"Loaded {len(findings)} findings from QIR")
225
+ except Exception as e:
226
+ logger.error(f"Failed to load QIR: {e}")
227
+
228
+ def load_patches(self, patches_dir: Path) -> None:
229
+ """Load patches for quick fixes."""
230
+ if not patches_dir.exists():
231
+ return
232
+
233
+ for patch_file in patches_dir.glob("*.patch"):
234
+ try:
235
+ patch_content = patch_file.read_text()
236
+ # Parse patch to extract file and changes
237
+ patch_info = self._parse_patch(patch_content, patch_file.stem)
238
+ if patch_info:
239
+ file_path = patch_info["file"]
240
+ if file_path not in self._patches_by_file:
241
+ self._patches_by_file[file_path] = []
242
+ self._patches_by_file[file_path].append(patch_info)
243
+ except Exception as e:
244
+ logger.warning(f"Failed to parse patch {patch_file}: {e}")
245
+
246
+ def _parse_patch(self, content: str, patch_id: str) -> Optional[Dict]:
247
+ """Parse a unified diff patch."""
248
+ lines = content.split("\n")
249
+ file_path = None
250
+ changes = []
251
+
252
+ for i, line in enumerate(lines):
253
+ if line.startswith("--- a/"):
254
+ file_path = line[6:]
255
+ elif line.startswith("+++ b/"):
256
+ file_path = line[6:]
257
+ elif line.startswith("@@"):
258
+ # Parse hunk header: @@ -start,count +start,count @@
259
+ try:
260
+ parts = line.split(" ")
261
+ new_range = parts[2] # +start,count
262
+ start = int(new_range.split(",")[0][1:]) - 1 # 0-indexed
263
+ changes.append({"start_line": start, "hunk_start": i})
264
+ except (IndexError, ValueError):
265
+ pass
266
+
267
+ if file_path:
268
+ return {
269
+ "id": patch_id,
270
+ "file": file_path,
271
+ "content": content,
272
+ "changes": changes,
273
+ }
274
+ return None
275
+
276
+ def _process_findings(self, findings: List[Dict]) -> None:
277
+ """Process findings into diagnostics."""
278
+ self._findings_by_file.clear()
279
+ self._diagnostics.clear()
280
+
281
+ for finding in findings:
282
+ file_path = finding.get("file_path")
283
+ if not file_path:
284
+ continue
285
+
286
+ if file_path not in self._findings_by_file:
287
+ self._findings_by_file[file_path] = []
288
+ self._findings_by_file[file_path].append(finding)
289
+
290
+ # Convert to diagnostics
291
+ for file_path, file_findings in self._findings_by_file.items():
292
+ self._diagnostics[file_path] = [self._finding_to_diagnostic(f) for f in file_findings]
293
+
294
+ def _finding_to_diagnostic(self, finding: Dict) -> LSPDiagnostic:
295
+ """Convert a QE finding to an LSP diagnostic."""
296
+ # Get line number (default to 0)
297
+ line = finding.get("line_number", finding.get("line_start", 1)) - 1
298
+ line = max(0, line)
299
+
300
+ # Create range (default to whole line)
301
+ start = LSPPosition(line=line, character=0)
302
+ end = LSPPosition(line=line, character=1000) # End of line
303
+
304
+ # Map severity
305
+ qe_severity = finding.get("severity", "medium").lower()
306
+ lsp_severity = QE_TO_LSP_SEVERITY.get(qe_severity, DiagnosticSeverity.WARNING)
307
+
308
+ # Build message
309
+ title = finding.get("title", "QE Finding")
310
+ description = finding.get("description", "")
311
+ message = f"{title}\n\n{description}" if description else title
312
+
313
+ return LSPDiagnostic(
314
+ range=LSPRange(start=start, end=end),
315
+ message=message,
316
+ severity=lsp_severity,
317
+ code=finding.get("id"),
318
+ source="superqode",
319
+ data={"finding": finding},
320
+ )
321
+
322
+ def get_diagnostics(self, uri: str) -> List[LSPDiagnostic]:
323
+ """Get diagnostics for a document."""
324
+ path = self._uri_to_path(uri)
325
+
326
+ # Try relative path first
327
+ rel_path = str(path)
328
+ if self.project_root:
329
+ try:
330
+ rel_path = str(path.relative_to(self.project_root))
331
+ except ValueError:
332
+ pass
333
+
334
+ return self._diagnostics.get(rel_path, [])
335
+
336
+ def get_code_actions(
337
+ self,
338
+ uri: str,
339
+ range_: LSPRange,
340
+ diagnostics: List[Dict],
341
+ ) -> List[CodeAction]:
342
+ """Get code actions for a range."""
343
+ actions: List[CodeAction] = []
344
+ path = self._uri_to_path(uri)
345
+
346
+ # Get relative path
347
+ rel_path = str(path)
348
+ if self.project_root:
349
+ try:
350
+ rel_path = str(path.relative_to(self.project_root))
351
+ except ValueError:
352
+ pass
353
+
354
+ # Check for patches
355
+ patches = self._patches_by_file.get(rel_path, [])
356
+ for patch in patches:
357
+ # Check if patch applies to this range
358
+ for change in patch.get("changes", []):
359
+ if change["start_line"] >= range_.start.line - 5:
360
+ action = CodeAction(
361
+ title=f"Apply QE fix: {patch['id']}",
362
+ kind=CodeActionKind.QUICKFIX,
363
+ is_preferred=True,
364
+ command={
365
+ "title": "Apply Patch",
366
+ "command": "superqode.applyPatch",
367
+ "arguments": [patch["id"], patch["content"]],
368
+ },
369
+ )
370
+ actions.append(action)
371
+ break
372
+
373
+ # Add suppress action for diagnostics
374
+ for diag_data in diagnostics:
375
+ finding_id = diag_data.get("code")
376
+ if finding_id:
377
+ action = CodeAction(
378
+ title=f"Suppress finding: {finding_id}",
379
+ kind=CodeActionKind.QUICKFIX,
380
+ command={
381
+ "title": "Suppress Finding",
382
+ "command": "superqode.suppressFinding",
383
+ "arguments": [finding_id],
384
+ },
385
+ )
386
+ actions.append(action)
387
+
388
+ # Add run QE action
389
+ actions.append(
390
+ CodeAction(
391
+ title="Run SuperQode QE on this file",
392
+ kind=CodeActionKind.SOURCE,
393
+ command={
394
+ "title": "Run QE",
395
+ "command": "superqode.runQE",
396
+ "arguments": [uri],
397
+ },
398
+ )
399
+ )
400
+
401
+ return actions
402
+
403
+ # ================================================================
404
+ # LSP Protocol Handling
405
+ # ================================================================
406
+
407
+ def handle_request(self, method: str, params: Dict) -> Any:
408
+ """Handle an LSP request."""
409
+ handlers = {
410
+ "initialize": self._handle_initialize,
411
+ "shutdown": self._handle_shutdown,
412
+ "textDocument/codeAction": self._handle_code_action,
413
+ "textDocument/hover": self._handle_hover,
414
+ "superqode/runQE": self._handle_run_qe,
415
+ "superqode/loadQIR": self._handle_load_qir,
416
+ }
417
+
418
+ handler = handlers.get(method)
419
+ if handler:
420
+ return handler(params)
421
+
422
+ logger.warning(f"Unhandled request: {method}")
423
+ return None
424
+
425
+ def handle_notification(self, method: str, params: Dict) -> None:
426
+ """Handle an LSP notification."""
427
+ handlers = {
428
+ "initialized": self._handle_initialized,
429
+ "exit": self._handle_exit,
430
+ "textDocument/didOpen": self._handle_did_open,
431
+ "textDocument/didClose": self._handle_did_close,
432
+ "textDocument/didChange": self._handle_did_change,
433
+ "textDocument/didSave": self._handle_did_save,
434
+ }
435
+
436
+ handler = handlers.get(method)
437
+ if handler:
438
+ handler(params)
439
+
440
+ def _handle_initialize(self, params: Dict) -> Dict:
441
+ """Handle initialize request."""
442
+ root_uri = params.get("rootUri")
443
+ if root_uri:
444
+ self.project_root = self._uri_to_path(root_uri)
445
+
446
+ self._initialized = True
447
+
448
+ return {
449
+ "capabilities": {
450
+ "textDocumentSync": {
451
+ "openClose": True,
452
+ "change": 1, # Full sync
453
+ "save": {"includeText": False},
454
+ },
455
+ "codeActionProvider": {
456
+ "codeActionKinds": [
457
+ CodeActionKind.QUICKFIX,
458
+ CodeActionKind.SOURCE,
459
+ ],
460
+ },
461
+ "hoverProvider": True,
462
+ "executeCommandProvider": {
463
+ "commands": [
464
+ "superqode.runQE",
465
+ "superqode.applyPatch",
466
+ "superqode.suppressFinding",
467
+ ],
468
+ },
469
+ },
470
+ "serverInfo": {
471
+ "name": "SuperQode LSP",
472
+ "version": "1.0.0",
473
+ },
474
+ }
475
+
476
+ def _handle_initialized(self, params: Dict) -> None:
477
+ """Handle initialized notification."""
478
+ logger.info("LSP client initialized")
479
+
480
+ # Load existing QIR if available
481
+ if self.project_root:
482
+ qr_dir = self.project_root / ".superqode" / "qe-artifacts" / "qr"
483
+ if qr_dir.exists():
484
+ qr_files = sorted(qir_dir.glob("*.json"), reverse=True)
485
+ if qir_files:
486
+ self.load_qir(qir_files[0])
487
+
488
+ # Load patches
489
+ patches_dir = self.project_root / ".superqode" / "qe-artifacts" / "patches"
490
+ self.load_patches(patches_dir)
491
+
492
+ def _handle_shutdown(self, params: Dict) -> None:
493
+ """Handle shutdown request."""
494
+ self._shutdown = True
495
+ return None
496
+
497
+ def _handle_exit(self, params: Dict) -> None:
498
+ """Handle exit notification."""
499
+ sys.exit(0 if self._shutdown else 1)
500
+
501
+ def _handle_did_open(self, params: Dict) -> None:
502
+ """Handle textDocument/didOpen."""
503
+ doc = params.get("textDocument", {})
504
+ uri = doc.get("uri", "")
505
+ text = doc.get("text", "")
506
+
507
+ self._open_documents[uri] = text
508
+
509
+ # Publish diagnostics for this file
510
+ self._publish_diagnostics(uri)
511
+
512
+ def _handle_did_close(self, params: Dict) -> None:
513
+ """Handle textDocument/didClose."""
514
+ doc = params.get("textDocument", {})
515
+ uri = doc.get("uri", "")
516
+
517
+ self._open_documents.pop(uri, None)
518
+
519
+ def _handle_did_change(self, params: Dict) -> None:
520
+ """Handle textDocument/didChange."""
521
+ doc = params.get("textDocument", {})
522
+ uri = doc.get("uri", "")
523
+ changes = params.get("contentChanges", [])
524
+
525
+ if changes:
526
+ # Full sync - take the last change
527
+ self._open_documents[uri] = changes[-1].get("text", "")
528
+
529
+ def _handle_did_save(self, params: Dict) -> None:
530
+ """Handle textDocument/didSave."""
531
+ doc = params.get("textDocument", {})
532
+ uri = doc.get("uri", "")
533
+
534
+ # Could trigger incremental QE here
535
+ logger.debug(f"Document saved: {uri}")
536
+
537
+ def _handle_code_action(self, params: Dict) -> List[Dict]:
538
+ """Handle textDocument/codeAction."""
539
+ doc = params.get("textDocument", {})
540
+ uri = doc.get("uri", "")
541
+ range_data = params.get("range", {})
542
+ context = params.get("context", {})
543
+
544
+ range_ = LSPRange.from_dict(range_data)
545
+ diagnostics = context.get("diagnostics", [])
546
+
547
+ actions = self.get_code_actions(uri, range_, diagnostics)
548
+ return [a.to_dict() for a in actions]
549
+
550
+ def _handle_hover(self, params: Dict) -> Optional[Dict]:
551
+ """Handle textDocument/hover."""
552
+ doc = params.get("textDocument", {})
553
+ uri = doc.get("uri", "")
554
+ position = params.get("position", {})
555
+
556
+ # Find finding at position
557
+ diagnostics = self.get_diagnostics(uri)
558
+ line = position.get("line", 0)
559
+
560
+ for diag in diagnostics:
561
+ if diag.range.start.line <= line <= diag.range.end.line:
562
+ finding = diag.data.get("finding", {}) if diag.data else {}
563
+
564
+ # Build hover content
565
+ content = [
566
+ f"**{finding.get('title', 'QE Finding')}**",
567
+ "",
568
+ f"Severity: {finding.get('severity', 'unknown')}",
569
+ f"Category: {finding.get('category', 'unknown')}",
570
+ "",
571
+ finding.get("description", ""),
572
+ ]
573
+
574
+ if finding.get("suggested_fix"):
575
+ content.extend(["", "**Suggested Fix:**", finding["suggested_fix"]])
576
+
577
+ return {
578
+ "contents": {
579
+ "kind": "markdown",
580
+ "value": "\n".join(content),
581
+ }
582
+ }
583
+
584
+ return None
585
+
586
+ def _handle_run_qe(self, params: Dict) -> Dict:
587
+ """Handle superqode/runQE request."""
588
+ uri = params.get("uri")
589
+ mode = params.get("mode", "quick")
590
+
591
+ if self._on_qe_request:
592
+ self._on_qe_request(uri)
593
+
594
+ return {"status": "started", "mode": mode}
595
+
596
+ def _handle_load_qir(self, params: Dict) -> Dict:
597
+ """Handle superqode/loadQIR request."""
598
+ qr_path = params.get("path")
599
+ if qr_path:
600
+ self.load_qir(Path(qr_path))
601
+
602
+ # Republish diagnostics
603
+ for uri in self._open_documents:
604
+ self._publish_diagnostics(uri)
605
+
606
+ return {"status": "loaded"}
607
+
608
+ return {"status": "error", "message": "No path provided"}
609
+
610
+ def _publish_diagnostics(self, uri: str) -> None:
611
+ """Publish diagnostics for a document."""
612
+ diagnostics = self.get_diagnostics(uri)
613
+ self._send_notification(
614
+ "textDocument/publishDiagnostics",
615
+ {
616
+ "uri": uri,
617
+ "diagnostics": [d.to_dict() for d in diagnostics],
618
+ },
619
+ )
620
+
621
+ # ================================================================
622
+ # Transport Layer
623
+ # ================================================================
624
+
625
+ def _send_response(self, request_id: int, result: Any) -> None:
626
+ """Send a response."""
627
+ message = {
628
+ "jsonrpc": "2.0",
629
+ "id": request_id,
630
+ "result": result,
631
+ }
632
+ self._write_message(message)
633
+
634
+ def _send_error(self, request_id: int, code: int, message: str) -> None:
635
+ """Send an error response."""
636
+ msg = {
637
+ "jsonrpc": "2.0",
638
+ "id": request_id,
639
+ "error": {"code": code, "message": message},
640
+ }
641
+ self._write_message(msg)
642
+
643
+ def _send_notification(self, method: str, params: Dict) -> None:
644
+ """Send a notification."""
645
+ message = {
646
+ "jsonrpc": "2.0",
647
+ "method": method,
648
+ "params": params,
649
+ }
650
+ self._write_message(message)
651
+
652
+ def _write_message(self, message: Dict) -> None:
653
+ """Write a message to the transport."""
654
+ content = json.dumps(message)
655
+ header = f"Content-Length: {len(content)}\r\n\r\n"
656
+
657
+ if self._stdout:
658
+ self._stdout.write(header.encode() + content.encode())
659
+ self._stdout.flush()
660
+
661
+ def _read_message(self) -> Optional[Dict]:
662
+ """Read a message from the transport."""
663
+ if not self._stdin:
664
+ return None
665
+
666
+ try:
667
+ # Read headers
668
+ headers = {}
669
+ while True:
670
+ line = self._stdin.readline().decode("utf-8")
671
+ if not line or line == "\r\n":
672
+ break
673
+ if ":" in line:
674
+ key, value = line.split(":", 1)
675
+ headers[key.strip().lower()] = value.strip()
676
+
677
+ # Read content
678
+ content_length = int(headers.get("content-length", 0))
679
+ if content_length > 0:
680
+ content = self._stdin.read(content_length).decode("utf-8")
681
+ return json.loads(content)
682
+
683
+ except Exception as e:
684
+ logger.error(f"Error reading message: {e}")
685
+
686
+ return None
687
+
688
+ def _process_message(self, message: Dict) -> None:
689
+ """Process an incoming message."""
690
+ method = message.get("method")
691
+ params = message.get("params", {})
692
+ request_id = message.get("id")
693
+
694
+ if request_id is not None:
695
+ # Request - needs response
696
+ try:
697
+ result = self.handle_request(method, params)
698
+ self._send_response(request_id, result)
699
+ except Exception as e:
700
+ logger.error(f"Error handling request {method}: {e}")
701
+ self._send_error(request_id, -32603, str(e))
702
+ else:
703
+ # Notification - no response
704
+ self.handle_notification(method, params)
705
+
706
+ def run_stdio(self) -> None:
707
+ """Run the server using stdio transport."""
708
+ self._stdin = sys.stdin.buffer
709
+ self._stdout = sys.stdout.buffer
710
+
711
+ logger.info("SuperQode LSP server started (stdio)")
712
+
713
+ while not self._shutdown:
714
+ message = self._read_message()
715
+ if message:
716
+ self._process_message(message)
717
+ else:
718
+ break
719
+
720
+ logger.info("SuperQode LSP server stopped")
721
+
722
+ def run_tcp(self) -> None:
723
+ """Run the server using TCP transport."""
724
+ import socket
725
+
726
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
727
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
728
+ server.bind(("127.0.0.1", self.port))
729
+ server.listen(1)
730
+
731
+ logger.info(f"SuperQode LSP server started on port {self.port}")
732
+
733
+ try:
734
+ while not self._shutdown:
735
+ conn, addr = server.accept()
736
+ logger.info(f"Client connected: {addr}")
737
+
738
+ # Handle connection
739
+ self._stdin = conn.makefile("rb")
740
+ self._stdout = conn.makefile("wb")
741
+
742
+ while not self._shutdown:
743
+ message = self._read_message()
744
+ if message:
745
+ self._process_message(message)
746
+ else:
747
+ break
748
+
749
+ conn.close()
750
+
751
+ finally:
752
+ server.close()
753
+
754
+ logger.info("SuperQode LSP server stopped")
755
+
756
+ def run(self) -> None:
757
+ """Run the server."""
758
+ if self.transport == "tcp":
759
+ self.run_tcp()
760
+ else:
761
+ self.run_stdio()
762
+
763
+
764
+ def start_lsp_server(
765
+ project_root: Optional[Path] = None,
766
+ transport: str = "stdio",
767
+ port: int = 9000,
768
+ ) -> None:
769
+ """Start the LSP server."""
770
+ server = SuperQodeLSPServer(
771
+ project_root=project_root,
772
+ transport=transport,
773
+ port=port,
774
+ )
775
+ server.run()