@trac3er/oh-my-god 1.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 (229) hide show
  1. package/.claude-plugin/marketplace.json +36 -0
  2. package/.claude-plugin/plugin.json +23 -0
  3. package/.claude-plugin/scripts/install.sh +49 -0
  4. package/.claude-plugin/scripts/uninstall.sh +80 -0
  5. package/.claude-plugin/scripts/update.sh +84 -0
  6. package/.mcp.json +20 -0
  7. package/LICENSE +21 -0
  8. package/OMG-setup.sh +1093 -0
  9. package/README.md +335 -0
  10. package/THIRD_PARTY_NOTICES.md +24 -0
  11. package/UPSTREAM_DIFF.md +20 -0
  12. package/agents/__init__.py +1 -0
  13. package/agents/_model_roles.yaml +26 -0
  14. package/agents/designer.md +67 -0
  15. package/agents/explore.md +60 -0
  16. package/agents/model_roles.py +196 -0
  17. package/agents/omg-api-builder.md +23 -0
  18. package/agents/omg-architect-mode.md +43 -0
  19. package/agents/omg-architect.md +13 -0
  20. package/agents/omg-backend-engineer.md +43 -0
  21. package/agents/omg-critic.md +16 -0
  22. package/agents/omg-database-engineer.md +43 -0
  23. package/agents/omg-escalation-router.md +17 -0
  24. package/agents/omg-executor.md +12 -0
  25. package/agents/omg-frontend-designer.md +42 -0
  26. package/agents/omg-implement-mode.md +50 -0
  27. package/agents/omg-infra-engineer.md +43 -0
  28. package/agents/omg-qa-tester.md +16 -0
  29. package/agents/omg-research-mode.md +43 -0
  30. package/agents/omg-security-auditor.md +43 -0
  31. package/agents/omg-testing-engineer.md +43 -0
  32. package/agents/plan.md +80 -0
  33. package/agents/quick_task.md +64 -0
  34. package/agents/reviewer.md +83 -0
  35. package/agents/task.md +71 -0
  36. package/commands/OMG:ccg.md +22 -0
  37. package/commands/OMG:compat.md +57 -0
  38. package/commands/OMG:crazy.md +125 -0
  39. package/commands/OMG:domain-init.md +11 -0
  40. package/commands/OMG:escalate.md +52 -0
  41. package/commands/OMG:health-check.md +45 -0
  42. package/commands/OMG:init.md +134 -0
  43. package/commands/OMG:mode.md +44 -0
  44. package/commands/OMG:project-init.md +11 -0
  45. package/commands/OMG:ralph-start.md +43 -0
  46. package/commands/OMG:ralph-stop.md +23 -0
  47. package/commands/OMG:teams.md +39 -0
  48. package/commands/ai-commit.md +113 -0
  49. package/commands/ccg.md +9 -0
  50. package/commands/create-agent.md +183 -0
  51. package/commands/omc-teams.md +9 -0
  52. package/commands/session-branch.md +85 -0
  53. package/commands/session-fork.md +53 -0
  54. package/commands/session-merge.md +134 -0
  55. package/commands/theme.md +44 -0
  56. package/config/lsp_languages.yaml +324 -0
  57. package/config/themes/catppuccin-frappe.yaml +14 -0
  58. package/config/themes/catppuccin-latte.yaml +14 -0
  59. package/config/themes/catppuccin-macchiato.yaml +14 -0
  60. package/config/themes/catppuccin-mocha.yaml +14 -0
  61. package/config/themes/dracula.yaml +14 -0
  62. package/config/themes/gruvbox-dark.yaml +14 -0
  63. package/config/themes/nord.yaml +14 -0
  64. package/config/themes/one-dark.yaml +14 -0
  65. package/config/themes/solarized-dark.yaml +14 -0
  66. package/config/themes/tokyo-night.yaml +14 -0
  67. package/control_plane/__init__.py +2 -0
  68. package/control_plane/openapi.yaml +109 -0
  69. package/control_plane/server.py +107 -0
  70. package/control_plane/service.py +148 -0
  71. package/crates/omg-natives/Cargo.toml +17 -0
  72. package/crates/omg-natives/src/clipboard.rs +5 -0
  73. package/crates/omg-natives/src/glob.rs +15 -0
  74. package/crates/omg-natives/src/grep.rs +15 -0
  75. package/crates/omg-natives/src/highlight.rs +15 -0
  76. package/crates/omg-natives/src/html.rs +14 -0
  77. package/crates/omg-natives/src/image.rs +5 -0
  78. package/crates/omg-natives/src/keys.rs +5 -0
  79. package/crates/omg-natives/src/lib.rs +36 -0
  80. package/crates/omg-natives/src/prof.rs +5 -0
  81. package/crates/omg-natives/src/ps.rs +5 -0
  82. package/crates/omg-natives/src/shell.rs +5 -0
  83. package/crates/omg-natives/src/task.rs +5 -0
  84. package/crates/omg-natives/src/text.rs +14 -0
  85. package/hooks/_agent_registry.py +421 -0
  86. package/hooks/_budget.py +31 -0
  87. package/hooks/_common.py +476 -0
  88. package/hooks/_learnings.py +126 -0
  89. package/hooks/_memory.py +103 -0
  90. package/hooks/circuit-breaker.py +270 -0
  91. package/hooks/config-guard.py +163 -0
  92. package/hooks/context_pressure.py +53 -0
  93. package/hooks/credential_store.py +801 -0
  94. package/hooks/fetch-rate-limits.py +212 -0
  95. package/hooks/firewall.py +48 -0
  96. package/hooks/hashline-formatter-bridge.py +224 -0
  97. package/hooks/hashline-injector.py +273 -0
  98. package/hooks/hashline-validator.py +216 -0
  99. package/hooks/idle-detector.py +95 -0
  100. package/hooks/intentgate-keyword-detector.py +188 -0
  101. package/hooks/magic-keyword-router.py +195 -0
  102. package/hooks/policy_engine.py +310 -0
  103. package/hooks/post-tool-failure.py +19 -0
  104. package/hooks/post-write.py +199 -0
  105. package/hooks/pre-compact.py +204 -0
  106. package/hooks/pre-tool-inject.py +98 -0
  107. package/hooks/prompt-enhancer.py +672 -0
  108. package/hooks/quality-runner.py +191 -0
  109. package/hooks/secret-guard.py +47 -0
  110. package/hooks/session-end-capture.py +137 -0
  111. package/hooks/session-start.py +275 -0
  112. package/hooks/shadow_manager.py +297 -0
  113. package/hooks/state_migration.py +209 -0
  114. package/hooks/stop-gate.py +7 -0
  115. package/hooks/stop_dispatcher.py +929 -0
  116. package/hooks/test-validator.py +138 -0
  117. package/hooks/todo-state-tracker.py +114 -0
  118. package/hooks/tool-ledger.py +126 -0
  119. package/hooks/trust_review.py +524 -0
  120. package/install.sh +9 -0
  121. package/omg_natives/__init__.py +186 -0
  122. package/omg_natives/_bindings.py +165 -0
  123. package/omg_natives/clipboard.py +36 -0
  124. package/omg_natives/glob.py +42 -0
  125. package/omg_natives/grep.py +61 -0
  126. package/omg_natives/highlight.py +54 -0
  127. package/omg_natives/html.py +157 -0
  128. package/omg_natives/image.py +51 -0
  129. package/omg_natives/keys.py +46 -0
  130. package/omg_natives/prof.py +39 -0
  131. package/omg_natives/ps.py +93 -0
  132. package/omg_natives/shell.py +58 -0
  133. package/omg_natives/task.py +41 -0
  134. package/omg_natives/text.py +50 -0
  135. package/package.json +26 -0
  136. package/plugins/README.md +82 -0
  137. package/plugins/advanced/commands/OMG:code-review.md +114 -0
  138. package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
  139. package/plugins/advanced/commands/OMG:handoff.md +115 -0
  140. package/plugins/advanced/commands/OMG:learn.md +110 -0
  141. package/plugins/advanced/commands/OMG:maintainer.md +31 -0
  142. package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
  143. package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
  144. package/plugins/advanced/commands/OMG:security-review.md +119 -0
  145. package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
  146. package/plugins/advanced/commands/OMG:ship.md +46 -0
  147. package/plugins/advanced/plugin.json +96 -0
  148. package/plugins/core/plugin.json +82 -0
  149. package/pytest.ini +5 -0
  150. package/registry/__init__.py +1 -0
  151. package/registry/verify_artifact.py +90 -0
  152. package/rules/contextual/architect-mode.md +9 -0
  153. package/rules/contextual/big-picture.md +20 -0
  154. package/rules/contextual/code-hygiene.md +26 -0
  155. package/rules/contextual/context-management.md +19 -0
  156. package/rules/contextual/context-minimization.md +32 -0
  157. package/rules/contextual/ddd-sdd.md +28 -0
  158. package/rules/contextual/dependency-safety.md +16 -0
  159. package/rules/contextual/doc-check.md +13 -0
  160. package/rules/contextual/implement-mode.md +9 -0
  161. package/rules/contextual/infra-safety.md +14 -0
  162. package/rules/contextual/outside-in.md +13 -0
  163. package/rules/contextual/persistent-mode.md +24 -0
  164. package/rules/contextual/research-mode.md +9 -0
  165. package/rules/contextual/security-domains.md +25 -0
  166. package/rules/contextual/vision-detection.md +27 -0
  167. package/rules/contextual/web-search.md +25 -0
  168. package/rules/contextual/write-verify.md +23 -0
  169. package/rules/core/00-truth.md +20 -0
  170. package/rules/core/01-surgical.md +19 -0
  171. package/rules/core/02-circuit-breaker.md +22 -0
  172. package/rules/core/03-ensemble.md +28 -0
  173. package/rules/core/04-testing.md +30 -0
  174. package/runtime/__init__.py +32 -0
  175. package/runtime/adapters/__init__.py +13 -0
  176. package/runtime/adapters/claude.py +60 -0
  177. package/runtime/adapters/gpt.py +53 -0
  178. package/runtime/adapters/local.py +53 -0
  179. package/runtime/business_workflow.py +220 -0
  180. package/runtime/compat.py +1299 -0
  181. package/runtime/custom_agent_loader.py +366 -0
  182. package/runtime/dispatcher.py +47 -0
  183. package/runtime/ecosystem.py +371 -0
  184. package/runtime/legacy_compat.py +7 -0
  185. package/runtime/omc_compat.py +7 -0
  186. package/runtime/omc_contract_snapshot.json +916 -0
  187. package/runtime/omg_compat_contract_snapshot.json +916 -0
  188. package/runtime/subagent_dispatcher.py +362 -0
  189. package/runtime/team_router.py +838 -0
  190. package/scripts/check-omc-contract-snapshot.py +12 -0
  191. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  192. package/scripts/check-omg-standalone-clean.py +102 -0
  193. package/scripts/legacy_to_omg_migrate.py +29 -0
  194. package/scripts/migrate-omc.py +464 -0
  195. package/scripts/omc_to_omg_migrate.py +12 -0
  196. package/scripts/omg.py +493 -0
  197. package/scripts/settings-merge.py +224 -0
  198. package/scripts/verify-no-omc.sh +5 -0
  199. package/scripts/verify-standalone.sh +21 -0
  200. package/templates/idea.yml +30 -0
  201. package/templates/policy.yaml +15 -0
  202. package/templates/profile.yaml +25 -0
  203. package/templates/runtime.yaml +12 -0
  204. package/templates/working-memory.md +17 -0
  205. package/tools/__init__.py +2 -0
  206. package/tools/browser_consent.py +289 -0
  207. package/tools/browser_stealth.py +481 -0
  208. package/tools/browser_tool.py +448 -0
  209. package/tools/changelog_generator.py +268 -0
  210. package/tools/commit_splitter.py +361 -0
  211. package/tools/config_discovery.py +151 -0
  212. package/tools/config_merger.py +449 -0
  213. package/tools/git_inspector.py +298 -0
  214. package/tools/lsp_client.py +275 -0
  215. package/tools/lsp_discovery.py +231 -0
  216. package/tools/lsp_operations.py +392 -0
  217. package/tools/python_repl.py +656 -0
  218. package/tools/python_sandbox.py +609 -0
  219. package/tools/search_providers/__init__.py +77 -0
  220. package/tools/search_providers/brave.py +115 -0
  221. package/tools/search_providers/exa.py +116 -0
  222. package/tools/search_providers/jina.py +104 -0
  223. package/tools/search_providers/perplexity.py +139 -0
  224. package/tools/search_providers/synthetic.py +74 -0
  225. package/tools/session_snapshot.py +736 -0
  226. package/tools/ssh_manager.py +912 -0
  227. package/tools/theme_engine.py +294 -0
  228. package/tools/theme_selector.py +137 -0
  229. package/tools/web_search.py +622 -0
@@ -0,0 +1,231 @@
1
+ """LSP Server Auto-Discovery Tool
2
+
3
+ Scans for available LSP servers based on language configurations.
4
+ Searches in: node_modules/.bin/, .venv/bin/, system PATH
5
+
6
+ Feature flag: OMG_LSP_TOOLS_ENABLED (default: False)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import os
14
+ import shutil
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Try to load YAML, fall back to JSON
22
+ try:
23
+ import yaml
24
+ HAS_YAML = True
25
+ except ImportError:
26
+ HAS_YAML = False
27
+
28
+
29
+ def _load_language_config(project_dir: str = ".") -> dict[str, Any]:
30
+ """Load language configuration from YAML or JSON.
31
+
32
+ Returns dict with 'languages' key containing list of language configs.
33
+ Falls back to empty dict if file not found.
34
+ """
35
+ config_path = Path(project_dir) / "config" / "lsp_languages.yaml"
36
+
37
+ if not config_path.exists():
38
+ return {"languages": []}
39
+
40
+ try:
41
+ if HAS_YAML:
42
+ with open(config_path, "r", encoding="utf-8") as f:
43
+ return yaml.safe_load(f) or {"languages": []}
44
+ else:
45
+ # Fallback: try to parse as JSON
46
+ with open(config_path, "r", encoding="utf-8") as f:
47
+ content = f.read()
48
+ # Simple YAML to JSON conversion for basic cases
49
+ # This is a minimal fallback - just try JSON first
50
+ try:
51
+ return json.loads(content)
52
+ except json.JSONDecodeError:
53
+ # If JSON fails, return empty config
54
+ return {"languages": []}
55
+ except Exception as e:
56
+ logger.error(f"Failed to load language config: {e}")
57
+ return {"languages": []}
58
+
59
+
60
+ def _expand_path(path_str: str) -> str:
61
+ """Expand ~ and environment variables in path."""
62
+ return os.path.expanduser(os.path.expandvars(path_str))
63
+
64
+
65
+ def _find_in_paths(binary_name: str, search_paths: list[str]) -> str | None:
66
+ """Find binary in list of paths.
67
+
68
+ Args:
69
+ binary_name: Name of binary to find (e.g., 'pylsp')
70
+ search_paths: List of paths to search (may contain ~ and env vars)
71
+
72
+ Returns:
73
+ Full path to binary if found, None otherwise
74
+ """
75
+ for path_str in search_paths:
76
+ expanded = _expand_path(path_str)
77
+
78
+ # If path is a directory, look for binary inside
79
+ if os.path.isdir(expanded):
80
+ binary_path = os.path.join(expanded, binary_name)
81
+ if os.path.isfile(binary_path) and os.access(binary_path, os.X_OK):
82
+ return binary_path
83
+ # If path is a file, check if it matches
84
+ elif os.path.isfile(expanded) and os.access(expanded, os.X_OK):
85
+ if os.path.basename(expanded) == binary_name:
86
+ return expanded
87
+
88
+ return None
89
+
90
+
91
+ def _find_in_system_path(binary_name: str) -> str | None:
92
+ """Find binary in system PATH.
93
+
94
+ Args:
95
+ binary_name: Name of binary to find
96
+
97
+ Returns:
98
+ Full path to binary if found, None otherwise
99
+ """
100
+ return shutil.which(binary_name)
101
+
102
+
103
+ def discover_lsp_servers(project_dir: str = ".") -> list[dict[str, Any]]:
104
+ """Discover available LSP servers in project.
105
+
106
+ Scans for LSP servers defined in config/lsp_languages.yaml.
107
+ For each language, checks discovery_paths and system PATH.
108
+
109
+ Args:
110
+ project_dir: Project directory to scan (default: current directory)
111
+
112
+ Returns:
113
+ List of dicts with keys:
114
+ - language: Language name
115
+ - server_command: Command to start server
116
+ - server_name: Human-readable server name
117
+ - found_at: Full path to server binary (or None if not found)
118
+ - available: Boolean indicating if server was found
119
+ """
120
+ config = _load_language_config(project_dir)
121
+ languages = config.get("languages", [])
122
+
123
+ discovered = []
124
+
125
+ for lang_config in languages:
126
+ language = lang_config.get("name", "unknown")
127
+ server_command = lang_config.get("server_command", [])
128
+ server_name = lang_config.get("server_name", "unknown")
129
+ discovery_paths = lang_config.get("discovery_paths", [])
130
+
131
+ # Get the binary name (first element of server_command)
132
+ if not server_command:
133
+ discovered.append({
134
+ "language": language,
135
+ "server_command": server_command,
136
+ "server_name": server_name,
137
+ "found_at": None,
138
+ "available": False,
139
+ })
140
+ continue
141
+
142
+ binary_name = server_command[0]
143
+
144
+ # Search in discovery_paths first
145
+ found_at = _find_in_paths(binary_name, discovery_paths)
146
+
147
+ # Fall back to system PATH
148
+ if not found_at:
149
+ found_at = _find_in_system_path(binary_name)
150
+
151
+ discovered.append({
152
+ "language": language,
153
+ "server_command": server_command,
154
+ "server_name": server_name,
155
+ "found_at": found_at,
156
+ "available": found_at is not None,
157
+ })
158
+
159
+ return discovered
160
+
161
+
162
+ def _is_enabled() -> bool:
163
+ """Check if LSP discovery is enabled via feature flag.
164
+
165
+ Checks env var first (OMG_LSP_TOOLS_ENABLED), then settings.json.
166
+ """
167
+ # Check environment variable
168
+ env_val = os.environ.get("OMG_LSP_TOOLS_ENABLED", "").lower()
169
+ if env_val in ("0", "false", "no"):
170
+ return False
171
+ if env_val in ("1", "true", "yes"):
172
+ return True
173
+
174
+ # Check settings.json
175
+ try:
176
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
177
+ settings_path = os.path.join(project_dir, "settings.json")
178
+ if os.path.exists(settings_path):
179
+ with open(settings_path, "r", encoding="utf-8") as f:
180
+ settings = json.load(f)
181
+ features = settings.get("_oal", {}).get("features", {})
182
+ if "LSP_TOOLS" in features:
183
+ return features["LSP_TOOLS"]
184
+ except Exception:
185
+ pass
186
+
187
+ # Default: disabled (opt-in)
188
+ return False
189
+
190
+
191
+ def main():
192
+ """CLI entry point for LSP discovery.
193
+
194
+ Usage:
195
+ python3 tools/lsp_discovery.py --project <dir>
196
+
197
+ Outputs JSON to stdout with list of discovered servers.
198
+ """
199
+ import argparse
200
+
201
+ parser = argparse.ArgumentParser(
202
+ description="Discover available LSP servers in project"
203
+ )
204
+ parser.add_argument(
205
+ "--project",
206
+ default=".",
207
+ help="Project directory to scan (default: current directory)"
208
+ )
209
+
210
+ args = parser.parse_args()
211
+
212
+ # Discover servers (works regardless of feature flag)
213
+ servers = discover_lsp_servers(args.project)
214
+
215
+ # Output as JSON
216
+ output = {
217
+ "project_dir": os.path.abspath(args.project),
218
+ "enabled": _is_enabled(),
219
+ "servers": servers,
220
+ "summary": {
221
+ "total": len(servers),
222
+ "available": sum(1 for s in servers if s["available"]),
223
+ "unavailable": sum(1 for s in servers if not s["available"]),
224
+ }
225
+ }
226
+
227
+ json.dump(output, sys.stdout, indent=2)
228
+
229
+
230
+ if __name__ == "__main__":
231
+ main()
@@ -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