@trac3er/oh-my-god 2.0.0 → 2.0.2

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 (243) hide show
  1. package/.claude-plugin/marketplace.json +8 -8
  2. package/.claude-plugin/plugin.json +5 -4
  3. package/.claude-plugin/scripts/uninstall.sh +74 -3
  4. package/.claude-plugin/scripts/update.sh +78 -3
  5. package/.coveragerc +26 -0
  6. package/.mcp.json +4 -4
  7. package/CHANGELOG.md +14 -0
  8. package/CODE_OF_CONDUCT.md +27 -0
  9. package/CONTRIBUTING.md +62 -0
  10. package/OMG-setup.sh +1201 -355
  11. package/README.md +77 -56
  12. package/SECURITY.md +25 -0
  13. package/agents/__init__.py +1 -0
  14. package/agents/model_roles.py +196 -0
  15. package/agents/omg-architect-mode.md +3 -5
  16. package/agents/omg-backend-engineer.md +3 -5
  17. package/agents/omg-database-engineer.md +3 -5
  18. package/agents/omg-frontend-designer.md +4 -5
  19. package/agents/omg-implement-mode.md +4 -5
  20. package/agents/omg-infra-engineer.md +3 -5
  21. package/agents/omg-research-mode.md +4 -6
  22. package/agents/omg-security-auditor.md +3 -5
  23. package/agents/omg-testing-engineer.md +3 -5
  24. package/build/lib/yaml.py +321 -0
  25. package/commands/OMG:ai-commit.md +101 -14
  26. package/commands/OMG:arch.md +302 -19
  27. package/commands/OMG:ccg.md +12 -7
  28. package/commands/OMG:compat.md +25 -17
  29. package/commands/OMG:cost.md +173 -13
  30. package/commands/OMG:crazy.md +1 -1
  31. package/commands/OMG:create-agent.md +170 -20
  32. package/commands/OMG:deps.md +235 -17
  33. package/commands/OMG:domain-init.md +1 -1
  34. package/commands/OMG:escalate.md +41 -12
  35. package/commands/OMG:health-check.md +37 -13
  36. package/commands/OMG:init.md +122 -14
  37. package/commands/OMG:project-init.md +1 -1
  38. package/commands/OMG:session-branch.md +76 -9
  39. package/commands/OMG:session-fork.md +42 -5
  40. package/commands/OMG:session-merge.md +124 -8
  41. package/commands/OMG:setup.md +69 -12
  42. package/commands/OMG:stats.md +215 -14
  43. package/commands/OMG:teams.md +19 -10
  44. package/config/lsp_languages.yaml +8 -0
  45. package/hooks/__init__.py +0 -0
  46. package/hooks/_agent_registry.py +423 -0
  47. package/hooks/_analytics.py +291 -0
  48. package/hooks/_budget.py +31 -0
  49. package/hooks/_common.py +569 -0
  50. package/hooks/_compression_optimizer.py +119 -0
  51. package/hooks/_cost_ledger.py +176 -0
  52. package/hooks/_learnings.py +126 -0
  53. package/hooks/_memory.py +103 -0
  54. package/hooks/_protected_context.py +150 -0
  55. package/hooks/_token_counter.py +221 -0
  56. package/hooks/branch_manager.py +236 -0
  57. package/hooks/budget_governor.py +232 -0
  58. package/hooks/circuit-breaker.py +270 -0
  59. package/hooks/compression_feedback.py +254 -0
  60. package/hooks/config-guard.py +216 -0
  61. package/hooks/context_pressure.py +53 -0
  62. package/hooks/credential_store.py +1020 -0
  63. package/hooks/fetch-rate-limits.py +212 -0
  64. package/hooks/firewall.py +48 -0
  65. package/hooks/hashline-formatter-bridge.py +224 -0
  66. package/hooks/hashline-injector.py +273 -0
  67. package/hooks/hashline-validator.py +216 -0
  68. package/hooks/idle-detector.py +95 -0
  69. package/hooks/intentgate-keyword-detector.py +188 -0
  70. package/hooks/magic-keyword-router.py +195 -0
  71. package/hooks/policy_engine.py +505 -0
  72. package/hooks/post-tool-failure.py +19 -0
  73. package/hooks/post-write.py +219 -0
  74. package/hooks/post_write.py +46 -0
  75. package/hooks/pre-compact.py +398 -0
  76. package/hooks/pre-tool-inject.py +98 -0
  77. package/hooks/prompt-enhancer.py +672 -0
  78. package/hooks/quality-runner.py +191 -0
  79. package/hooks/query.py +512 -0
  80. package/hooks/secret-guard.py +61 -0
  81. package/hooks/secret_audit.py +144 -0
  82. package/hooks/session-end-capture.py +137 -0
  83. package/hooks/session-start.py +277 -0
  84. package/hooks/setup_wizard.py +582 -0
  85. package/hooks/shadow_manager.py +297 -0
  86. package/hooks/state_migration.py +225 -0
  87. package/hooks/stop-gate.py +7 -0
  88. package/hooks/stop_dispatcher.py +945 -0
  89. package/hooks/test-validator.py +361 -0
  90. package/hooks/test_generator_hook.py +123 -0
  91. package/hooks/todo-state-tracker.py +114 -0
  92. package/hooks/tool-ledger.py +149 -0
  93. package/hooks/trust_review.py +585 -0
  94. package/hud/omg-hud.mjs +31 -1
  95. package/lab/__init__.py +1 -0
  96. package/lab/pipeline.py +75 -0
  97. package/lab/policies.py +52 -0
  98. package/package.json +7 -18
  99. package/plugins/README.md +33 -61
  100. package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
  101. package/plugins/advanced/commands/OMG:learn.md +1 -1
  102. package/plugins/advanced/commands/OMG:security-review.md +3 -3
  103. package/plugins/advanced/commands/OMG:ship.md +1 -1
  104. package/plugins/advanced/plugin.json +1 -1
  105. package/plugins/core/plugin.json +8 -3
  106. package/plugins/dephealth/__init__.py +0 -0
  107. package/plugins/dephealth/cve_scanner.py +188 -0
  108. package/plugins/dephealth/license_checker.py +135 -0
  109. package/plugins/dephealth/manifest_detector.py +423 -0
  110. package/plugins/dephealth/vuln_analyzer.py +169 -0
  111. package/plugins/testgen/__init__.py +0 -0
  112. package/plugins/testgen/codamosa_engine.py +402 -0
  113. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  114. package/plugins/testgen/framework_detector.py +271 -0
  115. package/plugins/testgen/skeleton_generator.py +219 -0
  116. package/plugins/viz/__init__.py +0 -0
  117. package/plugins/viz/ast_parser.py +139 -0
  118. package/plugins/viz/diagram_generator.py +192 -0
  119. package/plugins/viz/graph_builder.py +444 -0
  120. package/plugins/viz/native_parsers.py +259 -0
  121. package/plugins/viz/regex_parser.py +112 -0
  122. package/pyproject.toml +81 -0
  123. package/rules/contextual/write-verify.md +2 -2
  124. package/rules/core/00-truth.md +1 -1
  125. package/rules/core/01-surgical.md +1 -1
  126. package/rules/core/02-circuit-breaker.md +2 -2
  127. package/rules/core/03-ensemble.md +3 -3
  128. package/rules/core/04-testing.md +3 -3
  129. package/runtime/__init__.py +32 -0
  130. package/runtime/adapters/__init__.py +13 -0
  131. package/runtime/adapters/claude.py +60 -0
  132. package/runtime/adapters/gpt.py +53 -0
  133. package/runtime/adapters/local.py +53 -0
  134. package/runtime/adoption.py +212 -0
  135. package/runtime/business_workflow.py +220 -0
  136. package/runtime/cli_provider.py +85 -0
  137. package/runtime/compat.py +1299 -0
  138. package/runtime/custom_agent_loader.py +366 -0
  139. package/runtime/dispatcher.py +47 -0
  140. package/runtime/ecosystem.py +371 -0
  141. package/runtime/legacy_compat.py +7 -0
  142. package/runtime/mcp_config_writers.py +115 -0
  143. package/runtime/mcp_lifecycle.py +153 -0
  144. package/runtime/mcp_memory_server.py +135 -0
  145. package/runtime/memory_parsers/__init__.py +0 -0
  146. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  147. package/runtime/memory_parsers/claude_import.py +107 -0
  148. package/runtime/memory_parsers/export.py +97 -0
  149. package/runtime/memory_parsers/gemini_import.py +91 -0
  150. package/runtime/memory_parsers/kimi_import.py +91 -0
  151. package/runtime/memory_store.py +215 -0
  152. package/runtime/omc_compat.py +7 -0
  153. package/runtime/providers/__init__.py +0 -0
  154. package/runtime/providers/codex_provider.py +112 -0
  155. package/runtime/providers/gemini_provider.py +128 -0
  156. package/runtime/providers/kimi_provider.py +151 -0
  157. package/runtime/providers/opencode_provider.py +144 -0
  158. package/runtime/subagent_dispatcher.py +362 -0
  159. package/runtime/team_router.py +1167 -0
  160. package/runtime/tmux_session_manager.py +169 -0
  161. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  162. package/scripts/check-omg-contract-snapshot.py +12 -0
  163. package/scripts/check-omg-public-ready.py +193 -0
  164. package/scripts/check-omg-standalone-clean.py +103 -0
  165. package/scripts/legacy_to_omg_migrate.py +29 -0
  166. package/scripts/migrate-legacy.py +464 -0
  167. package/scripts/omc_to_omg_migrate.py +12 -0
  168. package/scripts/omg.py +492 -0
  169. package/scripts/settings-merge.py +283 -0
  170. package/scripts/verify-standalone.sh +8 -4
  171. package/settings.json +126 -29
  172. package/templates/profile.yaml +1 -1
  173. package/tools/__init__.py +2 -0
  174. package/tools/browser_consent.py +289 -0
  175. package/tools/browser_stealth.py +481 -0
  176. package/tools/browser_tool.py +448 -0
  177. package/tools/changelog_generator.py +347 -0
  178. package/tools/commit_splitter.py +746 -0
  179. package/tools/config_discovery.py +151 -0
  180. package/tools/config_merger.py +449 -0
  181. package/tools/dashboard_generator.py +300 -0
  182. package/tools/git_inspector.py +298 -0
  183. package/tools/lsp_client.py +275 -0
  184. package/tools/lsp_discovery.py +231 -0
  185. package/tools/lsp_operations.py +392 -0
  186. package/tools/pr_generator.py +404 -0
  187. package/tools/python_repl.py +656 -0
  188. package/tools/python_sandbox.py +609 -0
  189. package/tools/search_providers/__init__.py +77 -0
  190. package/tools/search_providers/brave.py +115 -0
  191. package/tools/search_providers/exa.py +116 -0
  192. package/tools/search_providers/jina.py +104 -0
  193. package/tools/search_providers/perplexity.py +139 -0
  194. package/tools/search_providers/synthetic.py +74 -0
  195. package/tools/session_snapshot.py +736 -0
  196. package/tools/ssh_manager.py +912 -0
  197. package/tools/theme_engine.py +294 -0
  198. package/tools/theme_selector.py +137 -0
  199. package/tools/web_search.py +622 -0
  200. package/yaml.py +321 -0
  201. package/.claude-plugin/scripts/install.sh +0 -9
  202. package/bun.lock +0 -23
  203. package/bunfig.toml +0 -3
  204. package/hooks/_budget.ts +0 -1
  205. package/hooks/_common.ts +0 -63
  206. package/hooks/circuit-breaker.ts +0 -101
  207. package/hooks/config-guard.ts +0 -4
  208. package/hooks/firewall.ts +0 -20
  209. package/hooks/policy_engine.ts +0 -156
  210. package/hooks/post-tool-failure.ts +0 -22
  211. package/hooks/post-write.ts +0 -4
  212. package/hooks/pre-tool-inject.ts +0 -4
  213. package/hooks/prompt-enhancer.ts +0 -46
  214. package/hooks/quality-runner.ts +0 -24
  215. package/hooks/secret-guard.ts +0 -4
  216. package/hooks/session-end-capture.ts +0 -19
  217. package/hooks/session-start.ts +0 -19
  218. package/hooks/shadow_manager.ts +0 -81
  219. package/hooks/stop-gate.ts +0 -22
  220. package/hooks/stop_dispatcher.ts +0 -147
  221. package/hooks/test-generator-hook.ts +0 -4
  222. package/hooks/tool-ledger.ts +0 -27
  223. package/hooks/trust_review.ts +0 -175
  224. package/lab/pipeline.ts +0 -75
  225. package/lab/policies.ts +0 -68
  226. package/runtime/common.ts +0 -111
  227. package/runtime/compat.ts +0 -174
  228. package/runtime/dispatcher.ts +0 -25
  229. package/runtime/ecosystem.ts +0 -186
  230. package/runtime/provider_bootstrap.ts +0 -99
  231. package/runtime/provider_smoke.ts +0 -34
  232. package/runtime/release_readiness.ts +0 -186
  233. package/runtime/team_router.ts +0 -144
  234. package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
  235. package/scripts/check-omg-standalone-clean.ts +0 -12
  236. package/scripts/check-runtime-clean.ts +0 -94
  237. package/scripts/omg.ts +0 -352
  238. package/scripts/settings-merge.ts +0 -93
  239. package/tools/commit_splitter.ts +0 -23
  240. package/tools/git_inspector.ts +0 -18
  241. package/tools/session_snapshot.ts +0 -47
  242. package/trac3er-oh-my-god-2.0.0.tgz +0 -0
  243. package/tsconfig.json +0 -15
@@ -0,0 +1,392 @@
1
+ """LSP operations — 11 high-level functions exposing LSP tools to agents.
2
+
3
+ Built on top of ``tools.lsp_client.LSPClient``. Each function:
4
+ - Returns a graceful default (empty list / None / False / dict) when disabled or on error.
5
+ - Never raises exceptions to callers.
6
+ - Checks the ``OMG_LSP_TOOLS_ENABLED`` feature flag via env var or settings.json.
7
+
8
+ Feature flag: ``OMG_LSP_TOOLS_ENABLED`` (default: False / opt-in only).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import os
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from tools.lsp_client import LSPClient
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Module-level singleton
24
+ # ---------------------------------------------------------------------------
25
+
26
+ _client: LSPClient | None = None
27
+
28
+ # LSP diagnostic severity codes → human-readable names
29
+ _SEVERITY_MAP: dict[int, str] = {
30
+ 1: "error",
31
+ 2: "warning",
32
+ 3: "information",
33
+ 4: "hint",
34
+ }
35
+
36
+ # LSP SymbolKind enum → human-readable names
37
+ _SYMBOL_KIND_MAP: dict[int, str] = {
38
+ 1: "File", 2: "Module", 3: "Namespace", 4: "Package",
39
+ 5: "Class", 6: "Method", 7: "Property", 8: "Field",
40
+ 9: "Constructor", 10: "Enum", 11: "Interface", 12: "Function",
41
+ 13: "Variable", 14: "Constant", 15: "String", 16: "Number",
42
+ 17: "Boolean", 18: "Array", 19: "Object", 20: "Key",
43
+ 21: "Null", 22: "EnumMember", 23: "Struct", 24: "Event",
44
+ 25: "Operator", 26: "TypeParameter",
45
+ }
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Internal helpers
50
+ # ---------------------------------------------------------------------------
51
+
52
+ def _is_enabled() -> bool:
53
+ """Check whether the LSP tools feature flag is on.
54
+
55
+ Resolution order mirrors ``hooks/_common.get_feature_flag``:
56
+ env var ``OMG_LSP_TOOLS_ENABLED`` → ``settings.json`` → default (False).
57
+ """
58
+ env_val = os.environ.get("OMG_LSP_TOOLS_ENABLED", "").lower()
59
+ if env_val in ("0", "false", "no"):
60
+ return False
61
+ if env_val in ("1", "true", "yes"):
62
+ return True
63
+
64
+ # Slow path: try get_feature_flag for settings.json support
65
+ try:
66
+ import sys as _sys
67
+
68
+ _hooks = os.path.join(
69
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
70
+ "hooks",
71
+ )
72
+ if _hooks not in _sys.path:
73
+ _sys.path.insert(0, _hooks)
74
+ from _common import get_feature_flag # type: ignore[import-untyped]
75
+
76
+ return get_feature_flag("LSP_TOOLS", default=False)
77
+ except Exception:
78
+ return False
79
+
80
+
81
+ def _file_uri(file_path: str) -> str:
82
+ """Convert a filesystem path to a ``file://`` URI."""
83
+ return Path(file_path).resolve().as_uri()
84
+
85
+
86
+ def _position_params(file_path: str, line: int, character: int) -> dict[str, Any]:
87
+ """Build ``TextDocumentPositionParams``."""
88
+ return {
89
+ "textDocument": {"uri": _file_uri(file_path)},
90
+ "position": {"line": line, "character": character},
91
+ }
92
+
93
+
94
+ def _normalize_locations(result: Any) -> list[dict]:
95
+ """Normalize an LSP Location / Location[] / LocationLink[] result."""
96
+ if result is None:
97
+ return []
98
+ if isinstance(result, dict):
99
+ return [{"uri": result.get("uri", ""), "range": result.get("range", {})}]
100
+ if isinstance(result, list):
101
+ locations: list[dict] = []
102
+ for item in result:
103
+ if isinstance(item, dict):
104
+ uri = item.get("uri", item.get("targetUri", ""))
105
+ range_ = item.get("range", item.get("targetRange", {}))
106
+ locations.append({"uri": uri, "range": range_})
107
+ return locations
108
+ return []
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Public API
113
+ # ---------------------------------------------------------------------------
114
+
115
+ def get_client() -> LSPClient:
116
+ """Return the module-level LSP client singleton (lazy-init).
117
+
118
+ Raises ``RuntimeError`` when ``OMG_LSP_TOOLS_ENABLED`` is False.
119
+ """
120
+ global _client
121
+ if not _is_enabled():
122
+ raise RuntimeError(
123
+ "LSP tools are disabled — set OMG_LSP_TOOLS_ENABLED=1 to enable"
124
+ )
125
+ if _client is None:
126
+ _client = LSPClient()
127
+ return _client
128
+
129
+
130
+ # -- 1. Diagnostics --------------------------------------------------------
131
+
132
+ def lsp_diagnostics(file_path: str) -> list[dict]:
133
+ """Pull diagnostics for *file_path*.
134
+
135
+ Returns a list of ``{severity, message, range}`` dicts.
136
+ """
137
+ try:
138
+ client = get_client()
139
+ result = client.send_request(
140
+ "textDocument/diagnostic",
141
+ {"textDocument": {"uri": _file_uri(file_path)}},
142
+ )
143
+ if result is None:
144
+ return []
145
+ items = result.get("items", [])
146
+ return [
147
+ {
148
+ "severity": _SEVERITY_MAP.get(d.get("severity", 1), "unknown"),
149
+ "message": d.get("message", ""),
150
+ "range": d.get("range", {}),
151
+ }
152
+ for d in items
153
+ if isinstance(d, dict)
154
+ ]
155
+ except Exception:
156
+ return []
157
+
158
+
159
+ # -- 2. Go to definition ---------------------------------------------------
160
+
161
+ def lsp_definition(file_path: str, line: int, character: int) -> list[dict]:
162
+ """Go-to-definition at the given position.
163
+
164
+ Returns a list of ``{uri, range}`` location dicts.
165
+ """
166
+ try:
167
+ client = get_client()
168
+ result = client.send_request(
169
+ "textDocument/definition", _position_params(file_path, line, character),
170
+ )
171
+ return _normalize_locations(result)
172
+ except Exception:
173
+ return []
174
+
175
+
176
+ # -- 3. Go to type definition ----------------------------------------------
177
+
178
+ def lsp_type_definition(file_path: str, line: int, character: int) -> list[dict]:
179
+ """Go-to-type-definition at the given position.
180
+
181
+ Returns a list of ``{uri, range}`` location dicts.
182
+ """
183
+ try:
184
+ client = get_client()
185
+ result = client.send_request(
186
+ "textDocument/typeDefinition",
187
+ _position_params(file_path, line, character),
188
+ )
189
+ return _normalize_locations(result)
190
+ except Exception:
191
+ return []
192
+
193
+
194
+ # -- 4. Go to implementation -----------------------------------------------
195
+
196
+ def lsp_implementation(file_path: str, line: int, character: int) -> list[dict]:
197
+ """Find implementations at the given position.
198
+
199
+ Returns a list of ``{uri, range}`` location dicts.
200
+ """
201
+ try:
202
+ client = get_client()
203
+ result = client.send_request(
204
+ "textDocument/implementation",
205
+ _position_params(file_path, line, character),
206
+ )
207
+ return _normalize_locations(result)
208
+ except Exception:
209
+ return []
210
+
211
+
212
+ # -- 5. Find references ----------------------------------------------------
213
+
214
+ def lsp_references(
215
+ file_path: str,
216
+ line: int,
217
+ character: int,
218
+ include_declaration: bool = True,
219
+ ) -> list[dict]:
220
+ """Find all references to the symbol at the given position.
221
+
222
+ Returns a list of ``{uri, range}`` location dicts.
223
+ """
224
+ try:
225
+ client = get_client()
226
+ params = _position_params(file_path, line, character)
227
+ params["context"] = {"includeDeclaration": include_declaration}
228
+ result = client.send_request("textDocument/references", params)
229
+ return _normalize_locations(result)
230
+ except Exception:
231
+ return []
232
+
233
+
234
+ # -- 6. Hover --------------------------------------------------------------
235
+
236
+ def lsp_hover(file_path: str, line: int, character: int) -> str | None:
237
+ """Hover information at the given position.
238
+
239
+ Returns the hover text as a string, or ``None``.
240
+ """
241
+ try:
242
+ client = get_client()
243
+ result = client.send_request(
244
+ "textDocument/hover", _position_params(file_path, line, character),
245
+ )
246
+ if result is None:
247
+ return None
248
+ contents = result.get("contents", "")
249
+ if isinstance(contents, str):
250
+ return contents
251
+ if isinstance(contents, dict):
252
+ return contents.get("value", str(contents))
253
+ if isinstance(contents, list):
254
+ parts: list[str] = []
255
+ for part in contents:
256
+ if isinstance(part, str):
257
+ parts.append(part)
258
+ elif isinstance(part, dict):
259
+ parts.append(part.get("value", str(part)))
260
+ return "\n".join(parts)
261
+ return str(contents)
262
+ except Exception:
263
+ return None
264
+
265
+
266
+ # -- 7. Document symbols ----------------------------------------------------
267
+
268
+ def lsp_symbols(file_path: str) -> list[dict]:
269
+ """Document symbols for *file_path*.
270
+
271
+ Returns a list of ``{name, kind, range}`` dicts.
272
+ """
273
+ try:
274
+ client = get_client()
275
+ result = client.send_request(
276
+ "textDocument/documentSymbol",
277
+ {"textDocument": {"uri": _file_uri(file_path)}},
278
+ )
279
+ if result is None:
280
+ return []
281
+ if not isinstance(result, list):
282
+ return []
283
+ return [
284
+ {
285
+ "name": sym.get("name", ""),
286
+ "kind": _SYMBOL_KIND_MAP.get(sym.get("kind", 0), "Unknown"),
287
+ "range": sym.get("range", sym.get("location", {}).get("range", {})),
288
+ }
289
+ for sym in result
290
+ if isinstance(sym, dict)
291
+ ]
292
+ except Exception:
293
+ return []
294
+
295
+
296
+ # -- 8. Rename --------------------------------------------------------------
297
+
298
+ def lsp_rename(
299
+ file_path: str, line: int, character: int, new_name: str,
300
+ ) -> dict:
301
+ """Rename the symbol at the given position.
302
+
303
+ Returns a workspace-edit dict (``{changes, documentChanges}``).
304
+ """
305
+ try:
306
+ client = get_client()
307
+ params = _position_params(file_path, line, character)
308
+ params["newName"] = new_name
309
+ result = client.send_request("textDocument/rename", params)
310
+ if result is None:
311
+ return {}
312
+ return result
313
+ except Exception:
314
+ return {}
315
+
316
+
317
+ # -- 9. Code actions --------------------------------------------------------
318
+
319
+ def lsp_code_actions(file_path: str, line: int, character: int) -> list[dict]:
320
+ """Available code actions at the given position.
321
+
322
+ Returns a list of ``{title, kind}`` dicts.
323
+ """
324
+ try:
325
+ client = get_client()
326
+ pos = {"line": line, "character": character}
327
+ result = client.send_request(
328
+ "textDocument/codeAction",
329
+ {
330
+ "textDocument": {"uri": _file_uri(file_path)},
331
+ "range": {"start": pos, "end": pos},
332
+ "context": {"diagnostics": []},
333
+ },
334
+ )
335
+ if result is None:
336
+ return []
337
+ if not isinstance(result, list):
338
+ return []
339
+ return [
340
+ {
341
+ "title": action.get("title", ""),
342
+ "kind": action.get("kind", ""),
343
+ }
344
+ for action in result
345
+ if isinstance(action, dict)
346
+ ]
347
+ except Exception:
348
+ return []
349
+
350
+
351
+ # -- 10. Status -------------------------------------------------------------
352
+
353
+ def lsp_status() -> dict:
354
+ """Return the current LSP client status.
355
+
356
+ Returns ``{connected: bool, server_name: str | None, capabilities: dict}``.
357
+ """
358
+ try:
359
+ if not _is_enabled():
360
+ return {"connected": False, "server_name": None, "capabilities": {}}
361
+ if _client is None:
362
+ return {"connected": False, "server_name": None, "capabilities": {}}
363
+ return {
364
+ "connected": _client.is_connected(),
365
+ "server_name": getattr(_client, "_server_name", None),
366
+ "capabilities": getattr(_client, "_capabilities", {}),
367
+ }
368
+ except Exception:
369
+ return {"connected": False, "server_name": None, "capabilities": {}}
370
+
371
+
372
+ # -- 11. Reload -------------------------------------------------------------
373
+
374
+ def lsp_reload() -> bool:
375
+ """Restart the LSP client singleton.
376
+
377
+ Shuts down any existing client, creates a fresh one.
378
+ Returns ``True`` on success, ``False`` on failure or disabled.
379
+ """
380
+ global _client
381
+ try:
382
+ if not _is_enabled():
383
+ return False
384
+ if _client is not None:
385
+ try:
386
+ _client.shutdown()
387
+ except Exception:
388
+ pass
389
+ _client = LSPClient()
390
+ return True
391
+ except Exception:
392
+ return False