@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.
- package/.claude-plugin/marketplace.json +8 -8
- package/.claude-plugin/plugin.json +5 -4
- package/.claude-plugin/scripts/uninstall.sh +74 -3
- package/.claude-plugin/scripts/update.sh +78 -3
- package/.coveragerc +26 -0
- package/.mcp.json +4 -4
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +62 -0
- package/OMG-setup.sh +1201 -355
- package/README.md +77 -56
- package/SECURITY.md +25 -0
- package/agents/__init__.py +1 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-architect-mode.md +3 -5
- package/agents/omg-backend-engineer.md +3 -5
- package/agents/omg-database-engineer.md +3 -5
- package/agents/omg-frontend-designer.md +4 -5
- package/agents/omg-implement-mode.md +4 -5
- package/agents/omg-infra-engineer.md +3 -5
- package/agents/omg-research-mode.md +4 -6
- package/agents/omg-security-auditor.md +3 -5
- package/agents/omg-testing-engineer.md +3 -5
- package/build/lib/yaml.py +321 -0
- package/commands/OMG:ai-commit.md +101 -14
- package/commands/OMG:arch.md +302 -19
- package/commands/OMG:ccg.md +12 -7
- package/commands/OMG:compat.md +25 -17
- package/commands/OMG:cost.md +173 -13
- package/commands/OMG:crazy.md +1 -1
- package/commands/OMG:create-agent.md +170 -20
- package/commands/OMG:deps.md +235 -17
- package/commands/OMG:domain-init.md +1 -1
- package/commands/OMG:escalate.md +41 -12
- package/commands/OMG:health-check.md +37 -13
- package/commands/OMG:init.md +122 -14
- package/commands/OMG:project-init.md +1 -1
- package/commands/OMG:session-branch.md +76 -9
- package/commands/OMG:session-fork.md +42 -5
- package/commands/OMG:session-merge.md +124 -8
- package/commands/OMG:setup.md +69 -12
- package/commands/OMG:stats.md +215 -14
- package/commands/OMG:teams.md +19 -10
- package/config/lsp_languages.yaml +8 -0
- package/hooks/__init__.py +0 -0
- package/hooks/_agent_registry.py +423 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +569 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +236 -0
- package/hooks/budget_governor.py +232 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +216 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +1020 -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 +505 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +219 -0
- package/hooks/post_write.py +46 -0
- package/hooks/pre-compact.py +398 -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/query.py +512 -0
- package/hooks/secret-guard.py +61 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +277 -0
- package/hooks/setup_wizard.py +582 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +225 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +945 -0
- package/hooks/test-validator.py +361 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +149 -0
- package/hooks/trust_review.py +585 -0
- package/hud/omg-hud.mjs +31 -1
- package/lab/__init__.py +1 -0
- package/lab/pipeline.py +75 -0
- package/lab/policies.py +52 -0
- package/package.json +7 -18
- package/plugins/README.md +33 -61
- package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
- package/plugins/advanced/commands/OMG:learn.md +1 -1
- package/plugins/advanced/commands/OMG:security-review.md +3 -3
- package/plugins/advanced/commands/OMG:ship.md +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +8 -3
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/cve_scanner.py +188 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +169 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +81 -0
- package/rules/contextual/write-verify.md +2 -2
- package/rules/core/00-truth.md +1 -1
- package/rules/core/01-surgical.md +1 -1
- package/rules/core/02-circuit-breaker.md +2 -2
- package/rules/core/03-ensemble.md +3 -3
- package/rules/core/04-testing.md +3 -3
- 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/adoption.py +212 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/cli_provider.py +85 -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/mcp_config_writers.py +115 -0
- package/runtime/mcp_lifecycle.py +153 -0
- package/runtime/mcp_memory_server.py +135 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +215 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/codex_provider.py +112 -0
- package/runtime/providers/gemini_provider.py +128 -0
- package/runtime/providers/kimi_provider.py +151 -0
- package/runtime/providers/opencode_provider.py +144 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +1167 -0
- package/runtime/tmux_session_manager.py +169 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +193 -0
- package/scripts/check-omg-standalone-clean.py +103 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +492 -0
- package/scripts/settings-merge.py +283 -0
- package/scripts/verify-standalone.sh +8 -4
- package/settings.json +126 -29
- package/templates/profile.yaml +1 -1
- 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 +347 -0
- package/tools/commit_splitter.py +746 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -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/pr_generator.py +404 -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
- package/yaml.py +321 -0
- package/.claude-plugin/scripts/install.sh +0 -9
- package/bun.lock +0 -23
- package/bunfig.toml +0 -3
- package/hooks/_budget.ts +0 -1
- package/hooks/_common.ts +0 -63
- package/hooks/circuit-breaker.ts +0 -101
- package/hooks/config-guard.ts +0 -4
- package/hooks/firewall.ts +0 -20
- package/hooks/policy_engine.ts +0 -156
- package/hooks/post-tool-failure.ts +0 -22
- package/hooks/post-write.ts +0 -4
- package/hooks/pre-tool-inject.ts +0 -4
- package/hooks/prompt-enhancer.ts +0 -46
- package/hooks/quality-runner.ts +0 -24
- package/hooks/secret-guard.ts +0 -4
- package/hooks/session-end-capture.ts +0 -19
- package/hooks/session-start.ts +0 -19
- package/hooks/shadow_manager.ts +0 -81
- package/hooks/stop-gate.ts +0 -22
- package/hooks/stop_dispatcher.ts +0 -147
- package/hooks/test-generator-hook.ts +0 -4
- package/hooks/tool-ledger.ts +0 -27
- package/hooks/trust_review.ts +0 -175
- package/lab/pipeline.ts +0 -75
- package/lab/policies.ts +0 -68
- package/runtime/common.ts +0 -111
- package/runtime/compat.ts +0 -174
- package/runtime/dispatcher.ts +0 -25
- package/runtime/ecosystem.ts +0 -186
- package/runtime/provider_bootstrap.ts +0 -99
- package/runtime/provider_smoke.ts +0 -34
- package/runtime/release_readiness.ts +0 -186
- package/runtime/team_router.ts +0 -144
- package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
- package/scripts/check-omg-standalone-clean.ts +0 -12
- package/scripts/check-runtime-clean.ts +0 -94
- package/scripts/omg.ts +0 -352
- package/scripts/settings-merge.ts +0 -93
- package/tools/commit_splitter.ts +0 -23
- package/tools/git_inspector.ts +0 -18
- package/tools/session_snapshot.ts +0 -47
- package/trac3er-oh-my-god-2.0.0.tgz +0 -0
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""LSP client wrapper supporting stdio transport.
|
|
2
|
+
|
|
3
|
+
Implements JSON-RPC 2.0 over stdio with Content-Length framing
|
|
4
|
+
per the Language Server Protocol specification.
|
|
5
|
+
|
|
6
|
+
Pure stdlib: subprocess, json, threading — no external dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import subprocess
|
|
14
|
+
import threading
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
_CONTENT_LENGTH_HEADER = "Content-Length: "
|
|
20
|
+
_HEADER_SEPARATOR = "\r\n\r\n"
|
|
21
|
+
_DEFAULT_TIMEOUT = 10.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LSPClient:
|
|
25
|
+
"""LSP client that communicates with a language server over stdio.
|
|
26
|
+
|
|
27
|
+
When ``server_cmd`` is None the client operates in **stub mode**:
|
|
28
|
+
``start()`` returns False, ``is_connected()`` returns False, and
|
|
29
|
+
``send_request()`` returns None. This allows OMG to instantiate
|
|
30
|
+
the client unconditionally — LSP is always optional.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
server_cmd: list[str] | None = None,
|
|
36
|
+
transport: str = "stdio",
|
|
37
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
38
|
+
) -> None:
|
|
39
|
+
if transport != "stdio":
|
|
40
|
+
raise ValueError(f"Unsupported transport: {transport!r} (only 'stdio' is supported)")
|
|
41
|
+
|
|
42
|
+
self._server_cmd = server_cmd
|
|
43
|
+
self._transport = transport
|
|
44
|
+
self._timeout = timeout
|
|
45
|
+
|
|
46
|
+
self._process: subprocess.Popen[bytes] | None = None
|
|
47
|
+
self._request_id = 0
|
|
48
|
+
self._lock = threading.Lock()
|
|
49
|
+
self._connected = False
|
|
50
|
+
self._initialized = False
|
|
51
|
+
|
|
52
|
+
def start(self) -> bool:
|
|
53
|
+
"""Start the language-server process.
|
|
54
|
+
|
|
55
|
+
Returns True on success, False if no server command was
|
|
56
|
+
configured (stub mode) or if the process failed to launch.
|
|
57
|
+
"""
|
|
58
|
+
if self._server_cmd is None:
|
|
59
|
+
logger.debug("LSPClient in stub mode — no server_cmd provided")
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
self._process = subprocess.Popen(
|
|
64
|
+
self._server_cmd,
|
|
65
|
+
stdin=subprocess.PIPE,
|
|
66
|
+
stdout=subprocess.PIPE,
|
|
67
|
+
stderr=subprocess.PIPE,
|
|
68
|
+
)
|
|
69
|
+
self._connected = True
|
|
70
|
+
logger.info("LSP server started: %s (pid=%d)", self._server_cmd, self._process.pid)
|
|
71
|
+
return True
|
|
72
|
+
except (OSError, FileNotFoundError) as exc:
|
|
73
|
+
logger.error("Failed to start LSP server %s: %s", self._server_cmd, exc)
|
|
74
|
+
self._connected = False
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def initialize(
|
|
78
|
+
self,
|
|
79
|
+
root_uri: str,
|
|
80
|
+
capabilities: dict[str, Any] | None = None,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Perform the LSP ``initialize`` handshake.
|
|
83
|
+
|
|
84
|
+
Returns the server capabilities dict, or an empty dict on
|
|
85
|
+
failure / stub mode.
|
|
86
|
+
"""
|
|
87
|
+
if not self._connected:
|
|
88
|
+
return {}
|
|
89
|
+
|
|
90
|
+
params: dict[str, Any] = {
|
|
91
|
+
"processId": None,
|
|
92
|
+
"rootUri": root_uri,
|
|
93
|
+
"capabilities": capabilities or {},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
result = self.send_request("initialize", params)
|
|
97
|
+
if result is None:
|
|
98
|
+
return {}
|
|
99
|
+
|
|
100
|
+
self.send_notification("initialized", {})
|
|
101
|
+
self.send_notification("initialized", {})
|
|
102
|
+
self._initialized = True
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
def shutdown(self) -> None:
|
|
106
|
+
"""Send ``shutdown`` then ``exit`` and tear down the process."""
|
|
107
|
+
if not self._connected or self._process is None:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
self.send_request("shutdown", {})
|
|
112
|
+
self.send_notification("exit", {})
|
|
113
|
+
except Exception: # noqa: BLE001
|
|
114
|
+
logger.debug("Error during LSP shutdown sequence", exc_info=True)
|
|
115
|
+
|
|
116
|
+
self._cleanup_process()
|
|
117
|
+
|
|
118
|
+
def send_request(
|
|
119
|
+
self,
|
|
120
|
+
method: str,
|
|
121
|
+
params: dict[str, Any],
|
|
122
|
+
) -> dict[str, Any] | None:
|
|
123
|
+
"""Send a JSON-RPC **request** (expects a response).
|
|
124
|
+
|
|
125
|
+
Returns the ``result`` field of the response, or None on
|
|
126
|
+
timeout / error / not connected.
|
|
127
|
+
"""
|
|
128
|
+
if not self._connected or self._process is None:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
with self._lock:
|
|
132
|
+
self._request_id += 1
|
|
133
|
+
req_id = self._request_id
|
|
134
|
+
|
|
135
|
+
message: dict[str, Any] = {
|
|
136
|
+
"jsonrpc": "2.0",
|
|
137
|
+
"id": req_id,
|
|
138
|
+
"method": method,
|
|
139
|
+
"params": params,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
self._write_message(message)
|
|
144
|
+
response = self._read_message()
|
|
145
|
+
except Exception: # noqa: BLE001
|
|
146
|
+
logger.debug("send_request(%s) failed", method, exc_info=True)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
if response is None:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
if "error" in response:
|
|
153
|
+
logger.warning(
|
|
154
|
+
"LSP error for %s: %s",
|
|
155
|
+
method,
|
|
156
|
+
response["error"],
|
|
157
|
+
)
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
return response.get("result")
|
|
161
|
+
|
|
162
|
+
def send_notification(
|
|
163
|
+
self,
|
|
164
|
+
method: str,
|
|
165
|
+
params: dict[str, Any],
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Send a JSON-RPC **notification** (no ``id``, no response)."""
|
|
168
|
+
if not self._connected or self._process is None:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
message: dict[str, Any] = {
|
|
172
|
+
"jsonrpc": "2.0",
|
|
173
|
+
"method": method,
|
|
174
|
+
"params": params,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
self._write_message(message)
|
|
179
|
+
except Exception: # noqa: BLE001
|
|
180
|
+
logger.debug("send_notification(%s) failed", method, exc_info=True)
|
|
181
|
+
|
|
182
|
+
def is_connected(self) -> bool:
|
|
183
|
+
"""Return True if the server process is alive and connected."""
|
|
184
|
+
if not self._connected or self._process is None:
|
|
185
|
+
return False
|
|
186
|
+
# Poll returns None while process is still running
|
|
187
|
+
return self._process.poll() is None
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def encode_message(body: dict[str, Any]) -> bytes:
|
|
191
|
+
"""Encode a JSON-RPC message with Content-Length header.
|
|
192
|
+
|
|
193
|
+
Public so tests can verify framing without a live process.
|
|
194
|
+
"""
|
|
195
|
+
payload = json.dumps(body, separators=(",", ":")).encode("utf-8")
|
|
196
|
+
header = f"Content-Length: {len(payload)}\r\n\r\n".encode("ascii")
|
|
197
|
+
return header + payload
|
|
198
|
+
|
|
199
|
+
def _write_message(self, body: dict[str, Any]) -> None:
|
|
200
|
+
"""Write a framed JSON-RPC message to the server's stdin."""
|
|
201
|
+
assert self._process is not None and self._process.stdin is not None
|
|
202
|
+
raw = self.encode_message(body)
|
|
203
|
+
self._process.stdin.write(raw)
|
|
204
|
+
self._process.stdin.flush()
|
|
205
|
+
|
|
206
|
+
def _read_message(self) -> dict[str, Any] | None:
|
|
207
|
+
"""Read one framed JSON-RPC message from the server's stdout.
|
|
208
|
+
|
|
209
|
+
Uses a background thread + join(timeout) so we never block
|
|
210
|
+
indefinitely.
|
|
211
|
+
"""
|
|
212
|
+
assert self._process is not None and self._process.stdout is not None
|
|
213
|
+
|
|
214
|
+
result_holder: list[dict[str, Any] | None] = [None]
|
|
215
|
+
|
|
216
|
+
def _reader() -> None:
|
|
217
|
+
try:
|
|
218
|
+
stdout = self._process.stdout # type: ignore[union-attr]
|
|
219
|
+
content_length = 0
|
|
220
|
+
content_length = 0
|
|
221
|
+
while True:
|
|
222
|
+
line = stdout.readline()
|
|
223
|
+
if not line:
|
|
224
|
+
return # EOF
|
|
225
|
+
line_str = line.decode("ascii").strip()
|
|
226
|
+
if not line_str:
|
|
227
|
+
break
|
|
228
|
+
if line_str.startswith(_CONTENT_LENGTH_HEADER.strip()):
|
|
229
|
+
content_length = int(line_str[len(_CONTENT_LENGTH_HEADER.strip()):])
|
|
230
|
+
|
|
231
|
+
if content_length == 0:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
data = stdout.read(content_length)
|
|
235
|
+
if data:
|
|
236
|
+
result_holder[0] = json.loads(data.decode("utf-8"))
|
|
237
|
+
except Exception: # noqa: BLE001
|
|
238
|
+
logger.debug("_reader failed", exc_info=True)
|
|
239
|
+
|
|
240
|
+
thread = threading.Thread(target=_reader, daemon=True)
|
|
241
|
+
thread.start()
|
|
242
|
+
thread.join(timeout=self._timeout)
|
|
243
|
+
|
|
244
|
+
if thread.is_alive():
|
|
245
|
+
logger.warning("LSP read timed out after %.1fs", self._timeout)
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
return result_holder[0]
|
|
249
|
+
|
|
250
|
+
def _cleanup_process(self) -> None:
|
|
251
|
+
"""Terminate / kill the server process and reset state."""
|
|
252
|
+
self._connected = False
|
|
253
|
+
self._initialized = False
|
|
254
|
+
if self._process is None:
|
|
255
|
+
return
|
|
256
|
+
try:
|
|
257
|
+
self._process.terminate()
|
|
258
|
+
self._process.wait(timeout=3)
|
|
259
|
+
except subprocess.TimeoutExpired:
|
|
260
|
+
self._process.kill()
|
|
261
|
+
self._process.wait(timeout=1)
|
|
262
|
+
except Exception: # noqa: BLE001
|
|
263
|
+
pass
|
|
264
|
+
finally:
|
|
265
|
+
self._process = None
|
|
266
|
+
|
|
267
|
+
def __enter__(self) -> LSPClient:
|
|
268
|
+
return self
|
|
269
|
+
|
|
270
|
+
def __exit__(self, *_: object) -> None:
|
|
271
|
+
self.shutdown()
|
|
272
|
+
|
|
273
|
+
def __repr__(self) -> str:
|
|
274
|
+
cmd = self._server_cmd or "(stub)"
|
|
275
|
+
return f"<LSPClient cmd={cmd!r} connected={self.is_connected()}>"
|
|
@@ -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()
|