@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.
- package/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +23 -0
- package/.claude-plugin/scripts/install.sh +49 -0
- package/.claude-plugin/scripts/uninstall.sh +80 -0
- package/.claude-plugin/scripts/update.sh +84 -0
- package/.mcp.json +20 -0
- package/LICENSE +21 -0
- package/OMG-setup.sh +1093 -0
- package/README.md +335 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/UPSTREAM_DIFF.md +20 -0
- package/agents/__init__.py +1 -0
- package/agents/_model_roles.yaml +26 -0
- package/agents/designer.md +67 -0
- package/agents/explore.md +60 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-api-builder.md +23 -0
- package/agents/omg-architect-mode.md +43 -0
- package/agents/omg-architect.md +13 -0
- package/agents/omg-backend-engineer.md +43 -0
- package/agents/omg-critic.md +16 -0
- package/agents/omg-database-engineer.md +43 -0
- package/agents/omg-escalation-router.md +17 -0
- package/agents/omg-executor.md +12 -0
- package/agents/omg-frontend-designer.md +42 -0
- package/agents/omg-implement-mode.md +50 -0
- package/agents/omg-infra-engineer.md +43 -0
- package/agents/omg-qa-tester.md +16 -0
- package/agents/omg-research-mode.md +43 -0
- package/agents/omg-security-auditor.md +43 -0
- package/agents/omg-testing-engineer.md +43 -0
- package/agents/plan.md +80 -0
- package/agents/quick_task.md +64 -0
- package/agents/reviewer.md +83 -0
- package/agents/task.md +71 -0
- package/commands/OMG:ccg.md +22 -0
- package/commands/OMG:compat.md +57 -0
- package/commands/OMG:crazy.md +125 -0
- package/commands/OMG:domain-init.md +11 -0
- package/commands/OMG:escalate.md +52 -0
- package/commands/OMG:health-check.md +45 -0
- package/commands/OMG:init.md +134 -0
- package/commands/OMG:mode.md +44 -0
- package/commands/OMG:project-init.md +11 -0
- package/commands/OMG:ralph-start.md +43 -0
- package/commands/OMG:ralph-stop.md +23 -0
- package/commands/OMG:teams.md +39 -0
- package/commands/ai-commit.md +113 -0
- package/commands/ccg.md +9 -0
- package/commands/create-agent.md +183 -0
- package/commands/omc-teams.md +9 -0
- package/commands/session-branch.md +85 -0
- package/commands/session-fork.md +53 -0
- package/commands/session-merge.md +134 -0
- package/commands/theme.md +44 -0
- package/config/lsp_languages.yaml +324 -0
- package/config/themes/catppuccin-frappe.yaml +14 -0
- package/config/themes/catppuccin-latte.yaml +14 -0
- package/config/themes/catppuccin-macchiato.yaml +14 -0
- package/config/themes/catppuccin-mocha.yaml +14 -0
- package/config/themes/dracula.yaml +14 -0
- package/config/themes/gruvbox-dark.yaml +14 -0
- package/config/themes/nord.yaml +14 -0
- package/config/themes/one-dark.yaml +14 -0
- package/config/themes/solarized-dark.yaml +14 -0
- package/config/themes/tokyo-night.yaml +14 -0
- package/control_plane/__init__.py +2 -0
- package/control_plane/openapi.yaml +109 -0
- package/control_plane/server.py +107 -0
- package/control_plane/service.py +148 -0
- package/crates/omg-natives/Cargo.toml +17 -0
- package/crates/omg-natives/src/clipboard.rs +5 -0
- package/crates/omg-natives/src/glob.rs +15 -0
- package/crates/omg-natives/src/grep.rs +15 -0
- package/crates/omg-natives/src/highlight.rs +15 -0
- package/crates/omg-natives/src/html.rs +14 -0
- package/crates/omg-natives/src/image.rs +5 -0
- package/crates/omg-natives/src/keys.rs +5 -0
- package/crates/omg-natives/src/lib.rs +36 -0
- package/crates/omg-natives/src/prof.rs +5 -0
- package/crates/omg-natives/src/ps.rs +5 -0
- package/crates/omg-natives/src/shell.rs +5 -0
- package/crates/omg-natives/src/task.rs +5 -0
- package/crates/omg-natives/src/text.rs +14 -0
- package/hooks/_agent_registry.py +421 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +476 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/config-guard.py +163 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +801 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +310 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +199 -0
- package/hooks/pre-compact.py +204 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/secret-guard.py +47 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +275 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +209 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +929 -0
- package/hooks/test-validator.py +138 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +126 -0
- package/hooks/trust_review.py +524 -0
- package/install.sh +9 -0
- package/omg_natives/__init__.py +186 -0
- package/omg_natives/_bindings.py +165 -0
- package/omg_natives/clipboard.py +36 -0
- package/omg_natives/glob.py +42 -0
- package/omg_natives/grep.py +61 -0
- package/omg_natives/highlight.py +54 -0
- package/omg_natives/html.py +157 -0
- package/omg_natives/image.py +51 -0
- package/omg_natives/keys.py +46 -0
- package/omg_natives/prof.py +39 -0
- package/omg_natives/ps.py +93 -0
- package/omg_natives/shell.py +58 -0
- package/omg_natives/task.py +41 -0
- package/omg_natives/text.py +50 -0
- package/package.json +26 -0
- package/plugins/README.md +82 -0
- package/plugins/advanced/commands/OMG:code-review.md +114 -0
- package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
- package/plugins/advanced/commands/OMG:handoff.md +115 -0
- package/plugins/advanced/commands/OMG:learn.md +110 -0
- package/plugins/advanced/commands/OMG:maintainer.md +31 -0
- package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
- package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
- package/plugins/advanced/commands/OMG:security-review.md +119 -0
- package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
- package/plugins/advanced/commands/OMG:ship.md +46 -0
- package/plugins/advanced/plugin.json +96 -0
- package/plugins/core/plugin.json +82 -0
- package/pytest.ini +5 -0
- package/registry/__init__.py +1 -0
- package/registry/verify_artifact.py +90 -0
- package/rules/contextual/architect-mode.md +9 -0
- package/rules/contextual/big-picture.md +20 -0
- package/rules/contextual/code-hygiene.md +26 -0
- package/rules/contextual/context-management.md +19 -0
- package/rules/contextual/context-minimization.md +32 -0
- package/rules/contextual/ddd-sdd.md +28 -0
- package/rules/contextual/dependency-safety.md +16 -0
- package/rules/contextual/doc-check.md +13 -0
- package/rules/contextual/implement-mode.md +9 -0
- package/rules/contextual/infra-safety.md +14 -0
- package/rules/contextual/outside-in.md +13 -0
- package/rules/contextual/persistent-mode.md +24 -0
- package/rules/contextual/research-mode.md +9 -0
- package/rules/contextual/security-domains.md +25 -0
- package/rules/contextual/vision-detection.md +27 -0
- package/rules/contextual/web-search.md +25 -0
- package/rules/contextual/write-verify.md +23 -0
- package/rules/core/00-truth.md +20 -0
- package/rules/core/01-surgical.md +19 -0
- package/rules/core/02-circuit-breaker.md +22 -0
- package/rules/core/03-ensemble.md +28 -0
- package/rules/core/04-testing.md +30 -0
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/omc_contract_snapshot.json +916 -0
- package/runtime/omg_compat_contract_snapshot.json +916 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +838 -0
- package/scripts/check-omc-contract-snapshot.py +12 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-standalone-clean.py +102 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-omc.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +493 -0
- package/scripts/settings-merge.py +224 -0
- package/scripts/verify-no-omc.sh +5 -0
- package/scripts/verify-standalone.sh +21 -0
- package/templates/idea.yml +30 -0
- package/templates/policy.yaml +15 -0
- package/templates/profile.yaml +25 -0
- package/templates/runtime.yaml +12 -0
- package/templates/working-memory.md +17 -0
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +268 -0
- package/tools/commit_splitter.py +361 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- 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
|