stravinsky 0.4.18__py3-none-any.whl → 0.4.66__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of stravinsky might be problematic. Click here for more details.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/auth/__init__.py +16 -6
- mcp_bridge/auth/cli.py +202 -11
- mcp_bridge/auth/oauth.py +1 -2
- mcp_bridge/auth/openai_oauth.py +4 -7
- mcp_bridge/auth/token_store.py +0 -1
- mcp_bridge/cli/__init__.py +1 -1
- mcp_bridge/cli/install_hooks.py +503 -107
- mcp_bridge/cli/session_report.py +0 -3
- mcp_bridge/config/__init__.py +2 -2
- mcp_bridge/config/hook_config.py +3 -5
- mcp_bridge/config/rate_limits.py +108 -13
- mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
- mcp_bridge/hooks/__init__.py +14 -4
- mcp_bridge/hooks/agent_reminder.py +4 -4
- mcp_bridge/hooks/auto_slash_command.py +5 -5
- mcp_bridge/hooks/budget_optimizer.py +2 -2
- mcp_bridge/hooks/claude_limits_hook.py +114 -0
- mcp_bridge/hooks/comment_checker.py +3 -4
- mcp_bridge/hooks/compaction.py +2 -2
- mcp_bridge/hooks/context.py +2 -1
- mcp_bridge/hooks/context_monitor.py +2 -2
- mcp_bridge/hooks/delegation_policy.py +85 -0
- mcp_bridge/hooks/directory_context.py +3 -3
- mcp_bridge/hooks/edit_recovery.py +3 -2
- mcp_bridge/hooks/edit_recovery_policy.py +49 -0
- mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
- mcp_bridge/hooks/events.py +160 -0
- mcp_bridge/hooks/git_noninteractive.py +4 -4
- mcp_bridge/hooks/keyword_detector.py +8 -10
- mcp_bridge/hooks/manager.py +35 -22
- mcp_bridge/hooks/notification_hook.py +13 -6
- mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
- mcp_bridge/hooks/parallel_enforcer.py +5 -5
- mcp_bridge/hooks/parallel_execution.py +22 -10
- mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
- mcp_bridge/hooks/pre_compact.py +8 -9
- mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
- mcp_bridge/hooks/preemptive_compaction.py +2 -3
- mcp_bridge/hooks/routing_notifications.py +80 -0
- mcp_bridge/hooks/rules_injector.py +11 -19
- mcp_bridge/hooks/session_idle.py +4 -4
- mcp_bridge/hooks/session_notifier.py +4 -4
- mcp_bridge/hooks/session_recovery.py +4 -5
- mcp_bridge/hooks/stravinsky_mode.py +1 -1
- mcp_bridge/hooks/subagent_stop.py +1 -3
- mcp_bridge/hooks/task_validator.py +2 -2
- mcp_bridge/hooks/tmux_manager.py +7 -8
- mcp_bridge/hooks/todo_delegation.py +4 -1
- mcp_bridge/hooks/todo_enforcer.py +180 -10
- mcp_bridge/hooks/truncation_policy.py +37 -0
- mcp_bridge/hooks/truncator.py +1 -2
- mcp_bridge/metrics/cost_tracker.py +115 -0
- mcp_bridge/native_search.py +93 -0
- mcp_bridge/native_watcher.py +118 -0
- mcp_bridge/notifications.py +3 -4
- mcp_bridge/orchestrator/enums.py +11 -0
- mcp_bridge/orchestrator/router.py +165 -0
- mcp_bridge/orchestrator/state.py +32 -0
- mcp_bridge/orchestrator/visualization.py +14 -0
- mcp_bridge/orchestrator/wisdom.py +34 -0
- mcp_bridge/prompts/__init__.py +1 -8
- mcp_bridge/prompts/dewey.py +1 -1
- mcp_bridge/prompts/planner.py +2 -4
- mcp_bridge/prompts/stravinsky.py +53 -31
- mcp_bridge/proxy/__init__.py +0 -0
- mcp_bridge/proxy/client.py +70 -0
- mcp_bridge/proxy/model_server.py +157 -0
- mcp_bridge/routing/__init__.py +43 -0
- mcp_bridge/routing/config.py +250 -0
- mcp_bridge/routing/model_tiers.py +135 -0
- mcp_bridge/routing/provider_state.py +261 -0
- mcp_bridge/routing/task_classifier.py +190 -0
- mcp_bridge/server.py +363 -34
- mcp_bridge/server_tools.py +298 -6
- mcp_bridge/tools/__init__.py +19 -8
- mcp_bridge/tools/agent_manager.py +549 -799
- mcp_bridge/tools/background_tasks.py +13 -17
- mcp_bridge/tools/code_search.py +54 -51
- mcp_bridge/tools/continuous_loop.py +0 -1
- mcp_bridge/tools/dashboard.py +19 -0
- mcp_bridge/tools/find_code.py +296 -0
- mcp_bridge/tools/init.py +1 -0
- mcp_bridge/tools/list_directory.py +42 -0
- mcp_bridge/tools/lsp/__init__.py +8 -8
- mcp_bridge/tools/lsp/manager.py +51 -28
- mcp_bridge/tools/lsp/tools.py +98 -65
- mcp_bridge/tools/model_invoke.py +1047 -152
- mcp_bridge/tools/mux_client.py +75 -0
- mcp_bridge/tools/project_context.py +1 -2
- mcp_bridge/tools/query_classifier.py +132 -49
- mcp_bridge/tools/read_file.py +84 -0
- mcp_bridge/tools/replace.py +45 -0
- mcp_bridge/tools/run_shell_command.py +38 -0
- mcp_bridge/tools/search_enhancements.py +347 -0
- mcp_bridge/tools/semantic_search.py +677 -92
- mcp_bridge/tools/session_manager.py +0 -2
- mcp_bridge/tools/skill_loader.py +0 -1
- mcp_bridge/tools/task_runner.py +5 -7
- mcp_bridge/tools/templates.py +3 -3
- mcp_bridge/tools/tool_search.py +331 -0
- mcp_bridge/tools/write_file.py +29 -0
- mcp_bridge/update_manager.py +33 -37
- mcp_bridge/update_manager_pypi.py +6 -8
- mcp_bridge/utils/cache.py +82 -0
- mcp_bridge/utils/process.py +71 -0
- mcp_bridge/utils/session_state.py +51 -0
- mcp_bridge/utils/truncation.py +76 -0
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/METADATA +84 -35
- stravinsky-0.4.66.dist-info/RECORD +198 -0
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
- stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
- stravinsky_claude_assets/agents/HOOKS.md +437 -0
- stravinsky_claude_assets/agents/code-reviewer.md +210 -0
- stravinsky_claude_assets/agents/comment_checker.md +580 -0
- stravinsky_claude_assets/agents/debugger.md +254 -0
- stravinsky_claude_assets/agents/delphi.md +495 -0
- stravinsky_claude_assets/agents/dewey.md +248 -0
- stravinsky_claude_assets/agents/explore.md +1198 -0
- stravinsky_claude_assets/agents/frontend.md +472 -0
- stravinsky_claude_assets/agents/implementation-lead.md +164 -0
- stravinsky_claude_assets/agents/momus.md +464 -0
- stravinsky_claude_assets/agents/research-lead.md +141 -0
- stravinsky_claude_assets/agents/stravinsky.md +730 -0
- stravinsky_claude_assets/commands/delphi.md +9 -0
- stravinsky_claude_assets/commands/dewey.md +54 -0
- stravinsky_claude_assets/commands/git-master.md +112 -0
- stravinsky_claude_assets/commands/index.md +49 -0
- stravinsky_claude_assets/commands/publish.md +86 -0
- stravinsky_claude_assets/commands/review.md +73 -0
- stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
- stravinsky_claude_assets/commands/str/agent_list.md +56 -0
- stravinsky_claude_assets/commands/str/agent_output.md +92 -0
- stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
- stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
- stravinsky_claude_assets/commands/str/cancel.md +51 -0
- stravinsky_claude_assets/commands/str/clean.md +97 -0
- stravinsky_claude_assets/commands/str/continue.md +38 -0
- stravinsky_claude_assets/commands/str/index.md +199 -0
- stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
- stravinsky_claude_assets/commands/str/search.md +205 -0
- stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
- stravinsky_claude_assets/commands/str/stats.md +71 -0
- stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
- stravinsky_claude_assets/commands/str/unwatch.md +42 -0
- stravinsky_claude_assets/commands/str/watch.md +45 -0
- stravinsky_claude_assets/commands/strav.md +53 -0
- stravinsky_claude_assets/commands/stravinsky.md +292 -0
- stravinsky_claude_assets/commands/verify.md +60 -0
- stravinsky_claude_assets/commands/version.md +5 -0
- stravinsky_claude_assets/hooks/README.md +248 -0
- stravinsky_claude_assets/hooks/comment_checker.py +193 -0
- stravinsky_claude_assets/hooks/context.py +38 -0
- stravinsky_claude_assets/hooks/context_monitor.py +153 -0
- stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
- stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
- stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
- stravinsky_claude_assets/hooks/notification_hook.py +103 -0
- stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
- stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
- stravinsky_claude_assets/hooks/pre_compact.py +123 -0
- stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
- stravinsky_claude_assets/hooks/session_recovery.py +263 -0
- stravinsky_claude_assets/hooks/stop_hook.py +89 -0
- stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
- stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
- stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
- stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
- stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
- stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
- stravinsky_claude_assets/hooks/truncator.py +23 -0
- stravinsky_claude_assets/rules/deployment_safety.md +51 -0
- stravinsky_claude_assets/rules/integration_wiring.md +89 -0
- stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
- stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
- stravinsky_claude_assets/settings.json +152 -0
- stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
- stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
- stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
- stravinsky_claude_assets/task_dependencies.json +34 -0
- stravinsky-0.4.18.dist-info/RECORD +0 -88
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
mcp_bridge/tools/lsp/__init__.py
CHANGED
|
@@ -4,20 +4,20 @@ LSP Tools Package
|
|
|
4
4
|
Provides Language Server Protocol functionality for code intelligence.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from .manager import LSPManager, get_lsp_manager
|
|
7
8
|
from .tools import (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
lsp_find_references,
|
|
9
|
+
lsp_code_action_resolve,
|
|
10
|
+
lsp_code_actions,
|
|
11
11
|
lsp_document_symbols,
|
|
12
|
-
|
|
12
|
+
lsp_extract_refactor,
|
|
13
|
+
lsp_find_references,
|
|
14
|
+
lsp_goto_definition,
|
|
15
|
+
lsp_hover,
|
|
13
16
|
lsp_prepare_rename,
|
|
14
17
|
lsp_rename,
|
|
15
|
-
lsp_code_actions,
|
|
16
|
-
lsp_code_action_resolve,
|
|
17
|
-
lsp_extract_refactor,
|
|
18
18
|
lsp_servers,
|
|
19
|
+
lsp_workspace_symbols,
|
|
19
20
|
)
|
|
20
|
-
from .manager import LSPManager, get_lsp_manager
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"lsp_hover",
|
mcp_bridge/tools/lsp/manager.py
CHANGED
|
@@ -13,24 +13,19 @@ Architecture:
|
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
|
-
import json
|
|
17
16
|
import logging
|
|
18
17
|
import os
|
|
19
|
-
import shlex
|
|
20
|
-
import signal
|
|
21
18
|
import threading
|
|
22
19
|
import time
|
|
23
20
|
from dataclasses import dataclass, field
|
|
24
|
-
from
|
|
25
|
-
from typing import Any, Optional
|
|
21
|
+
from typing import Optional
|
|
26
22
|
|
|
27
|
-
from pygls.client import JsonRPCClient
|
|
28
23
|
from lsprotocol.types import (
|
|
29
|
-
InitializeParams,
|
|
30
|
-
InitializedParams,
|
|
31
24
|
ClientCapabilities,
|
|
32
|
-
|
|
25
|
+
InitializedParams,
|
|
26
|
+
InitializeParams,
|
|
33
27
|
)
|
|
28
|
+
from pygls.client import JsonRPCClient
|
|
34
29
|
|
|
35
30
|
logger = logging.getLogger(__name__)
|
|
36
31
|
|
|
@@ -48,10 +43,11 @@ class LSPServer:
|
|
|
48
43
|
|
|
49
44
|
name: str
|
|
50
45
|
command: list[str]
|
|
51
|
-
client:
|
|
46
|
+
client: JsonRPCClient | None = None
|
|
52
47
|
initialized: bool = False
|
|
53
|
-
process:
|
|
54
|
-
pid:
|
|
48
|
+
process: asyncio.subprocess.Process | None = None
|
|
49
|
+
pid: int | None = None # Track subprocess PID for explicit cleanup
|
|
50
|
+
root_path: str | None = None # Track root path server was started with
|
|
55
51
|
last_used: float = field(default_factory=time.time) # Timestamp of last usage
|
|
56
52
|
created_at: float = field(default_factory=time.time) # Timestamp of server creation
|
|
57
53
|
|
|
@@ -82,24 +78,31 @@ class LSPManager:
|
|
|
82
78
|
self._servers: dict[str, LSPServer] = {}
|
|
83
79
|
self._lock = asyncio.Lock()
|
|
84
80
|
self._restart_attempts: dict[str, int] = {}
|
|
85
|
-
self._health_monitor_task:
|
|
81
|
+
self._health_monitor_task: asyncio.Task | None = None
|
|
86
82
|
|
|
87
83
|
# Register available LSP servers
|
|
88
84
|
self._register_servers()
|
|
89
85
|
|
|
90
86
|
def _register_servers(self):
|
|
91
87
|
"""Register available LSP server configurations."""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
# Allow overriding commands via environment variables
|
|
89
|
+
python_cmd = os.environ.get("LSP_CMD_PYTHON", "jedi-language-server").split()
|
|
90
|
+
ts_cmd = os.environ.get(
|
|
91
|
+
"LSP_CMD_TYPESCRIPT", "typescript-language-server --stdio"
|
|
92
|
+
).split()
|
|
93
|
+
|
|
94
|
+
self._servers["python"] = LSPServer(name="python", command=python_cmd)
|
|
95
|
+
self._servers["typescript"] = LSPServer(name="typescript", command=ts_cmd)
|
|
96
|
+
|
|
97
|
+
async def get_server(
|
|
98
|
+
self, language: str, root_path: str | None = None
|
|
99
|
+
) -> JsonRPCClient | None:
|
|
98
100
|
"""
|
|
99
101
|
Get or start a persistent LSP server for the given language.
|
|
100
102
|
|
|
101
103
|
Args:
|
|
102
104
|
language: Language identifier (e.g., "python", "typescript")
|
|
105
|
+
root_path: Project root path (optional, but recommended)
|
|
103
106
|
|
|
104
107
|
Returns:
|
|
105
108
|
JsonRPCClient instance or None if server unavailable
|
|
@@ -110,6 +113,20 @@ class LSPManager:
|
|
|
110
113
|
|
|
111
114
|
server = self._servers[language]
|
|
112
115
|
|
|
116
|
+
# Check if we need to restart due to root path change
|
|
117
|
+
# (Simple implementation: if root_path differs, restart)
|
|
118
|
+
# In multi-root workspaces, this might be too aggressive, but safe for now.
|
|
119
|
+
restart_needed = False
|
|
120
|
+
if root_path and server.root_path and root_path != server.root_path:
|
|
121
|
+
logger.info(
|
|
122
|
+
f"Restarting {language} LSP server: root path changed ({server.root_path} -> {root_path})"
|
|
123
|
+
)
|
|
124
|
+
restart_needed = True
|
|
125
|
+
|
|
126
|
+
if restart_needed:
|
|
127
|
+
async with self._lock:
|
|
128
|
+
await self._shutdown_single_server(language, server)
|
|
129
|
+
|
|
113
130
|
# Return existing initialized server
|
|
114
131
|
if server.initialized and server.client:
|
|
115
132
|
# Update last_used timestamp
|
|
@@ -129,7 +146,7 @@ class LSPManager:
|
|
|
129
146
|
return server.client
|
|
130
147
|
|
|
131
148
|
try:
|
|
132
|
-
await self._start_server(server)
|
|
149
|
+
await self._start_server(server, root_path)
|
|
133
150
|
# Start health monitor on first server creation
|
|
134
151
|
if self._health_monitor_task is None or self._health_monitor_task.done():
|
|
135
152
|
self._health_monitor_task = asyncio.create_task(self._background_health_monitor())
|
|
@@ -138,7 +155,7 @@ class LSPManager:
|
|
|
138
155
|
logger.error(f"Failed to start {language} LSP server: {e}")
|
|
139
156
|
return None
|
|
140
157
|
|
|
141
|
-
async def _start_server(self, server: LSPServer):
|
|
158
|
+
async def _start_server(self, server: LSPServer, root_path: str | None = None):
|
|
142
159
|
"""
|
|
143
160
|
Start a persistent LSP server process.
|
|
144
161
|
|
|
@@ -150,6 +167,7 @@ class LSPManager:
|
|
|
150
167
|
|
|
151
168
|
Args:
|
|
152
169
|
server: LSPServer metadata object
|
|
170
|
+
root_path: Project root path
|
|
153
171
|
"""
|
|
154
172
|
try:
|
|
155
173
|
# Create pygls client
|
|
@@ -158,7 +176,9 @@ class LSPManager:
|
|
|
158
176
|
logger.info(f"Starting {server.name} LSP server: {' '.join(server.command)}")
|
|
159
177
|
|
|
160
178
|
# Start server process (start_io expects cmd as first arg, then *args)
|
|
161
|
-
|
|
179
|
+
# Use cwd=root_path if available to help server find config
|
|
180
|
+
cwd = root_path if root_path and os.path.isdir(root_path) else None
|
|
181
|
+
await client.start_io(server.command[0], *server.command[1:], cwd=cwd)
|
|
162
182
|
|
|
163
183
|
# Brief delay for process startup
|
|
164
184
|
await asyncio.sleep(0.2)
|
|
@@ -180,8 +200,9 @@ class LSPManager:
|
|
|
180
200
|
)
|
|
181
201
|
|
|
182
202
|
# Perform LSP initialization handshake
|
|
203
|
+
root_uri = f"file://{root_path}" if root_path else None
|
|
183
204
|
init_params = InitializeParams(
|
|
184
|
-
process_id=None, root_uri=
|
|
205
|
+
process_id=None, root_uri=root_uri, capabilities=ClientCapabilities()
|
|
185
206
|
)
|
|
186
207
|
|
|
187
208
|
try:
|
|
@@ -195,12 +216,13 @@ class LSPManager:
|
|
|
195
216
|
|
|
196
217
|
logger.info(f"{server.name} LSP server initialized: {response}")
|
|
197
218
|
|
|
198
|
-
except
|
|
219
|
+
except TimeoutError:
|
|
199
220
|
raise ConnectionError(f"{server.name} LSP server initialization timed out")
|
|
200
221
|
|
|
201
222
|
# Store client reference (GC protection)
|
|
202
223
|
server.client = client
|
|
203
224
|
server.initialized = True
|
|
225
|
+
server.root_path = root_path
|
|
204
226
|
server.created_at = time.time()
|
|
205
227
|
server.last_used = time.time()
|
|
206
228
|
|
|
@@ -221,6 +243,7 @@ class LSPManager:
|
|
|
221
243
|
server.initialized = False
|
|
222
244
|
server.process = None
|
|
223
245
|
server.pid = None
|
|
246
|
+
server.root_path = None
|
|
224
247
|
raise
|
|
225
248
|
|
|
226
249
|
async def _restart_with_backoff(self, server: LSPServer):
|
|
@@ -281,7 +304,7 @@ class LSPManager:
|
|
|
281
304
|
)
|
|
282
305
|
logger.debug(f"{server.name} LSP server health check passed")
|
|
283
306
|
return True
|
|
284
|
-
except
|
|
307
|
+
except TimeoutError:
|
|
285
308
|
logger.warning(f"{server.name} LSP server health check timed out")
|
|
286
309
|
return False
|
|
287
310
|
except Exception as e:
|
|
@@ -307,7 +330,7 @@ class LSPManager:
|
|
|
307
330
|
await asyncio.wait_for(
|
|
308
331
|
server.client.protocol.send_request_async("shutdown", None), timeout=5.0
|
|
309
332
|
)
|
|
310
|
-
except
|
|
333
|
+
except TimeoutError:
|
|
311
334
|
logger.warning(f"{name} LSP server shutdown request timed out")
|
|
312
335
|
|
|
313
336
|
# Send exit notification
|
|
@@ -325,7 +348,7 @@ class LSPManager:
|
|
|
325
348
|
server.process.terminate()
|
|
326
349
|
try:
|
|
327
350
|
await asyncio.wait_for(server.process.wait(), timeout=2.0)
|
|
328
|
-
except
|
|
351
|
+
except TimeoutError:
|
|
329
352
|
server.process.kill()
|
|
330
353
|
await asyncio.wait_for(server.process.wait(), timeout=1.0)
|
|
331
354
|
except Exception as e:
|
|
@@ -433,7 +456,7 @@ class LSPManager:
|
|
|
433
456
|
|
|
434
457
|
|
|
435
458
|
# Singleton accessor
|
|
436
|
-
_manager_instance:
|
|
459
|
+
_manager_instance: LSPManager | None = None
|
|
437
460
|
_manager_lock = threading.Lock()
|
|
438
461
|
|
|
439
462
|
|
mcp_bridge/tools/lsp/tools.py
CHANGED
|
@@ -8,11 +8,12 @@ Supplements Claude Code's native LSP support with advanced operations.
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
-
import
|
|
11
|
+
import os
|
|
12
12
|
import sys
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
15
15
|
from urllib.parse import unquote, urlparse
|
|
16
|
+
from mcp_bridge.utils.process import async_execute
|
|
16
17
|
|
|
17
18
|
# Use lsprotocol for types
|
|
18
19
|
try:
|
|
@@ -26,6 +27,7 @@ try:
|
|
|
26
27
|
HoverParams,
|
|
27
28
|
Location,
|
|
28
29
|
Position,
|
|
30
|
+
PrepareRenameParams,
|
|
29
31
|
Range,
|
|
30
32
|
ReferenceContext,
|
|
31
33
|
ReferenceParams,
|
|
@@ -34,7 +36,6 @@ try:
|
|
|
34
36
|
TextDocumentItem,
|
|
35
37
|
TextDocumentPositionParams,
|
|
36
38
|
WorkspaceSymbolParams,
|
|
37
|
-
PrepareRenameParams,
|
|
38
39
|
)
|
|
39
40
|
except ImportError:
|
|
40
41
|
# Fallback/Mock for environment without lsprotocol
|
|
@@ -66,9 +67,44 @@ def _get_language_for_file(file_path: str) -> str:
|
|
|
66
67
|
return mapping.get(suffix, "unknown")
|
|
67
68
|
|
|
68
69
|
|
|
70
|
+
def _find_project_root(file_path: str) -> str | None:
|
|
71
|
+
"""
|
|
72
|
+
Find project root by looking for marker files.
|
|
73
|
+
|
|
74
|
+
Markers:
|
|
75
|
+
- Python: pyproject.toml, setup.py, requirements.txt, .git
|
|
76
|
+
- JS/TS: package.json, tsconfig.json, .git
|
|
77
|
+
- General: .git
|
|
78
|
+
"""
|
|
79
|
+
path = Path(file_path).resolve()
|
|
80
|
+
if path.is_file():
|
|
81
|
+
path = path.parent
|
|
82
|
+
|
|
83
|
+
markers = {
|
|
84
|
+
"pyproject.toml",
|
|
85
|
+
"setup.py",
|
|
86
|
+
"requirements.txt",
|
|
87
|
+
"package.json",
|
|
88
|
+
"tsconfig.json",
|
|
89
|
+
".git",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Walk up the tree
|
|
93
|
+
current = path
|
|
94
|
+
for _ in range(20): # Limit depth
|
|
95
|
+
for marker in markers:
|
|
96
|
+
if (current / marker).exists():
|
|
97
|
+
return str(current)
|
|
98
|
+
if current.parent == current: # Root reached
|
|
99
|
+
break
|
|
100
|
+
current = current.parent
|
|
101
|
+
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
69
105
|
async def _get_client_and_params(
|
|
70
106
|
file_path: str, needs_open: bool = True
|
|
71
|
-
) ->
|
|
107
|
+
) -> tuple[Any | None, str | None, str]:
|
|
72
108
|
"""
|
|
73
109
|
Get LSP client and prepare file for operations.
|
|
74
110
|
|
|
@@ -80,8 +116,14 @@ async def _get_client_and_params(
|
|
|
80
116
|
return None, None, "unknown"
|
|
81
117
|
|
|
82
118
|
lang = _get_language_for_file(file_path)
|
|
119
|
+
root_path = _find_project_root(file_path)
|
|
120
|
+
|
|
121
|
+
# Use found root or fallback to file's parent directory
|
|
122
|
+
# Passing root_path allows the manager to initialize/restart server with correct context
|
|
123
|
+
server_root = root_path if root_path else str(path.parent)
|
|
124
|
+
|
|
83
125
|
manager = get_lsp_manager()
|
|
84
|
-
client = await manager.get_server(lang)
|
|
126
|
+
client = await manager.get_server(lang, root_path=server_root)
|
|
85
127
|
|
|
86
128
|
if not client:
|
|
87
129
|
return None, None, lang
|
|
@@ -148,7 +190,7 @@ async def lsp_hover(file_path: str, line: int, character: int) -> str:
|
|
|
148
190
|
try:
|
|
149
191
|
if lang == "python":
|
|
150
192
|
# Use jedi for Python hover info
|
|
151
|
-
result =
|
|
193
|
+
result = await async_execute(
|
|
152
194
|
[
|
|
153
195
|
"python",
|
|
154
196
|
"-c",
|
|
@@ -157,14 +199,12 @@ import jedi
|
|
|
157
199
|
script = jedi.Script(path='{file_path}')
|
|
158
200
|
completions = script.infer({line}, {character})
|
|
159
201
|
for c in completions[:1]:
|
|
160
|
-
|
|
161
|
-
|
|
202
|
+
print(f"Type: {{c.type}}")
|
|
203
|
+
print(f"Name: {{c.full_name}}")
|
|
162
204
|
if c.docstring():
|
|
163
|
-
|
|
205
|
+
print(f"\\nDocstring:\\n{{c.docstring()[:500]}}")
|
|
164
206
|
""",
|
|
165
207
|
],
|
|
166
|
-
capture_output=True,
|
|
167
|
-
text=True,
|
|
168
208
|
timeout=10,
|
|
169
209
|
)
|
|
170
210
|
output = result.stdout.strip()
|
|
@@ -173,14 +213,14 @@ for c in completions[:1]:
|
|
|
173
213
|
return f"No hover info at line {line}, character {character}"
|
|
174
214
|
|
|
175
215
|
elif lang in ("typescript", "javascript", "typescriptreact", "javascriptreact"):
|
|
176
|
-
return
|
|
216
|
+
return "TypeScript hover requires running language server. Use Claude Code's native hover."
|
|
177
217
|
|
|
178
218
|
else:
|
|
179
219
|
return f"Hover not available for language: {lang}"
|
|
180
220
|
|
|
181
221
|
except FileNotFoundError as e:
|
|
182
222
|
return f"Tool not found: {e.filename}. Install jedi: pip install jedi"
|
|
183
|
-
except
|
|
223
|
+
except asyncio.TimeoutError:
|
|
184
224
|
return "Hover lookup timed out"
|
|
185
225
|
except Exception as e:
|
|
186
226
|
return f"Error: {str(e)}"
|
|
@@ -240,7 +280,7 @@ async def lsp_goto_definition(file_path: str, line: int, character: int) -> str:
|
|
|
240
280
|
|
|
241
281
|
try:
|
|
242
282
|
if lang == "python":
|
|
243
|
-
result =
|
|
283
|
+
result = await async_execute(
|
|
244
284
|
[
|
|
245
285
|
"python",
|
|
246
286
|
"-c",
|
|
@@ -249,11 +289,9 @@ import jedi
|
|
|
249
289
|
script = jedi.Script(path='{file_path}')
|
|
250
290
|
definitions = script.goto({line}, {character})
|
|
251
291
|
for d in definitions:
|
|
252
|
-
|
|
292
|
+
print(f"{{d.module_path}}:{{d.line}}:{{d.column}} - {{d.full_name}}")
|
|
253
293
|
""",
|
|
254
294
|
],
|
|
255
|
-
capture_output=True,
|
|
256
|
-
text=True,
|
|
257
295
|
timeout=10,
|
|
258
296
|
)
|
|
259
297
|
output = result.stdout.strip()
|
|
@@ -267,9 +305,9 @@ for d in definitions:
|
|
|
267
305
|
else:
|
|
268
306
|
return f"Goto definition not available for language: {lang}"
|
|
269
307
|
|
|
270
|
-
except FileNotFoundError
|
|
271
|
-
return
|
|
272
|
-
except
|
|
308
|
+
except FileNotFoundError:
|
|
309
|
+
return "Tool not found: Install jedi: pip install jedi"
|
|
310
|
+
except asyncio.TimeoutError:
|
|
273
311
|
return "Definition lookup timed out"
|
|
274
312
|
except Exception as e:
|
|
275
313
|
return f"Error: {str(e)}"
|
|
@@ -328,7 +366,7 @@ async def lsp_find_references(
|
|
|
328
366
|
|
|
329
367
|
try:
|
|
330
368
|
if lang == "python":
|
|
331
|
-
result =
|
|
369
|
+
result = await async_execute(
|
|
332
370
|
[
|
|
333
371
|
"python",
|
|
334
372
|
"-c",
|
|
@@ -337,13 +375,11 @@ import jedi
|
|
|
337
375
|
script = jedi.Script(path='{file_path}')
|
|
338
376
|
references = script.get_references({line}, {character}, include_builtins=False)
|
|
339
377
|
for r in references[:30]:
|
|
340
|
-
|
|
378
|
+
print(f"{{r.module_path}}:{{r.line}}:{{r.column}}")
|
|
341
379
|
if len(references) > 30:
|
|
342
|
-
|
|
380
|
+
print(f"... and {{len(references) - 30}} more")
|
|
343
381
|
""",
|
|
344
382
|
],
|
|
345
|
-
capture_output=True,
|
|
346
|
-
text=True,
|
|
347
383
|
timeout=15,
|
|
348
384
|
)
|
|
349
385
|
output = result.stdout.strip()
|
|
@@ -354,7 +390,7 @@ if len(references) > 30:
|
|
|
354
390
|
else:
|
|
355
391
|
return f"Find references not available for language: {lang}"
|
|
356
392
|
|
|
357
|
-
except
|
|
393
|
+
except asyncio.TimeoutError:
|
|
358
394
|
return "Reference search timed out"
|
|
359
395
|
except Exception as e:
|
|
360
396
|
return f"Error: {str(e)}"
|
|
@@ -425,7 +461,7 @@ async def lsp_document_symbols(file_path: str) -> str:
|
|
|
425
461
|
|
|
426
462
|
try:
|
|
427
463
|
if lang == "python":
|
|
428
|
-
result =
|
|
464
|
+
result = await async_execute(
|
|
429
465
|
[
|
|
430
466
|
"python",
|
|
431
467
|
"-c",
|
|
@@ -435,11 +471,9 @@ script = jedi.Script(path='{file_path}')
|
|
|
435
471
|
names = script.get_names(all_scopes=True, definitions=True)
|
|
436
472
|
for n in names:
|
|
437
473
|
indent = " " * (n.get_line_code().count(" ") if n.get_line_code() else 0)
|
|
438
|
-
|
|
474
|
+
print(f"{{n.line:4d}} | {{indent}}{{n.type:10}} {{n.name}}")
|
|
439
475
|
""",
|
|
440
476
|
],
|
|
441
|
-
capture_output=True,
|
|
442
|
-
text=True,
|
|
443
477
|
timeout=10,
|
|
444
478
|
)
|
|
445
479
|
output = result.stdout.strip()
|
|
@@ -449,10 +483,8 @@ for n in names:
|
|
|
449
483
|
|
|
450
484
|
else:
|
|
451
485
|
# Fallback: use ctags
|
|
452
|
-
result =
|
|
486
|
+
result = await async_execute(
|
|
453
487
|
["ctags", "-x", "--sort=no", str(path)],
|
|
454
|
-
capture_output=True,
|
|
455
|
-
text=True,
|
|
456
488
|
timeout=10,
|
|
457
489
|
)
|
|
458
490
|
output = result.stdout.strip()
|
|
@@ -462,7 +494,7 @@ for n in names:
|
|
|
462
494
|
|
|
463
495
|
except FileNotFoundError:
|
|
464
496
|
return "Install jedi (pip install jedi) or ctags for symbol lookup"
|
|
465
|
-
except
|
|
497
|
+
except asyncio.TimeoutError:
|
|
466
498
|
return "Symbol lookup timed out"
|
|
467
499
|
except Exception as e:
|
|
468
500
|
return f"Error: {str(e)}"
|
|
@@ -507,10 +539,8 @@ async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
|
507
539
|
# Fallback to legacy grep/ctags
|
|
508
540
|
try:
|
|
509
541
|
# Use ctags to index and grep for symbols
|
|
510
|
-
result =
|
|
542
|
+
result = await async_execute(
|
|
511
543
|
["rg", "-l", query, directory, "--type", "py", "--type", "ts", "--type", "js"],
|
|
512
|
-
capture_output=True,
|
|
513
|
-
text=True,
|
|
514
544
|
timeout=15,
|
|
515
545
|
)
|
|
516
546
|
|
|
@@ -524,10 +554,8 @@ async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
|
524
554
|
if not f:
|
|
525
555
|
continue
|
|
526
556
|
# Get symbols from each file
|
|
527
|
-
ctags_result =
|
|
557
|
+
ctags_result = await async_execute(
|
|
528
558
|
["ctags", "-x", "--sort=no", f],
|
|
529
|
-
capture_output=True,
|
|
530
|
-
text=True,
|
|
531
559
|
timeout=5,
|
|
532
560
|
)
|
|
533
561
|
for line in ctags_result.stdout.split("\n"):
|
|
@@ -540,7 +568,7 @@ async def lsp_workspace_symbols(query: str, directory: str = ".") -> str:
|
|
|
540
568
|
|
|
541
569
|
except FileNotFoundError:
|
|
542
570
|
return "Install ctags and ripgrep for workspace symbol search"
|
|
543
|
-
except
|
|
571
|
+
except asyncio.TimeoutError:
|
|
544
572
|
return "Search timed out"
|
|
545
573
|
except Exception as e:
|
|
546
574
|
return f"Error: {str(e)}"
|
|
@@ -587,7 +615,7 @@ async def lsp_prepare_rename(file_path: str, line: int, character: int) -> str:
|
|
|
587
615
|
|
|
588
616
|
try:
|
|
589
617
|
if lang == "python":
|
|
590
|
-
result =
|
|
618
|
+
result = await async_execute(
|
|
591
619
|
[
|
|
592
620
|
"python",
|
|
593
621
|
"-c",
|
|
@@ -596,16 +624,14 @@ import jedi
|
|
|
596
624
|
script = jedi.Script(path='{file_path}')
|
|
597
625
|
refs = script.get_references({line}, {character})
|
|
598
626
|
if refs:
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
627
|
+
print(f"Symbol: {{refs[0].name}}")
|
|
628
|
+
print(f"Type: {{refs[0].type}}")
|
|
629
|
+
print(f"References: {{len(refs)}}")
|
|
630
|
+
print("✅ Rename is valid")
|
|
603
631
|
else:
|
|
604
|
-
|
|
632
|
+
print("❌ No symbol found at position")
|
|
605
633
|
""",
|
|
606
634
|
],
|
|
607
|
-
capture_output=True,
|
|
608
|
-
text=True,
|
|
609
635
|
timeout=10,
|
|
610
636
|
)
|
|
611
637
|
return result.stdout.strip() or "No symbol found at position"
|
|
@@ -689,7 +715,7 @@ async def lsp_rename(
|
|
|
689
715
|
|
|
690
716
|
try:
|
|
691
717
|
if lang == "python":
|
|
692
|
-
result =
|
|
718
|
+
result = await async_execute(
|
|
693
719
|
[
|
|
694
720
|
"python",
|
|
695
721
|
"-c",
|
|
@@ -698,13 +724,11 @@ import jedi
|
|
|
698
724
|
script = jedi.Script(path='{file_path}')
|
|
699
725
|
refactoring = script.rename({line}, {character}, new_name='{new_name}')
|
|
700
726
|
for path, changed in refactoring.get_changed_files().items():
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
727
|
+
print(f"File: {{path}}")
|
|
728
|
+
print(changed[:500])
|
|
729
|
+
print("---")
|
|
704
730
|
""",
|
|
705
731
|
],
|
|
706
|
-
capture_output=True,
|
|
707
|
-
text=True,
|
|
708
732
|
timeout=15,
|
|
709
733
|
)
|
|
710
734
|
output = result.stdout.strip()
|
|
@@ -722,7 +746,7 @@ for path, changed in refactoring.get_changed_files().items():
|
|
|
722
746
|
return f"Error: {str(e)}"
|
|
723
747
|
|
|
724
748
|
|
|
725
|
-
def _apply_workspace_edit(changes:
|
|
749
|
+
def _apply_workspace_edit(changes: dict[str, list[Any]]):
|
|
726
750
|
"""Apply LSP changes to files."""
|
|
727
751
|
for file_uri, edits in changes.items():
|
|
728
752
|
parsed = urlparse(file_uri)
|
|
@@ -809,10 +833,8 @@ async def lsp_code_actions(file_path: str, line: int, character: int) -> str:
|
|
|
809
833
|
try:
|
|
810
834
|
if lang == "python":
|
|
811
835
|
# Use ruff to suggest fixes
|
|
812
|
-
result =
|
|
836
|
+
result = await async_execute(
|
|
813
837
|
["ruff", "check", str(path), "--output-format=json", "--show-fixes"],
|
|
814
|
-
capture_output=True,
|
|
815
|
-
text=True,
|
|
816
838
|
timeout=10,
|
|
817
839
|
)
|
|
818
840
|
|
|
@@ -868,10 +890,8 @@ async def lsp_code_action_resolve(file_path: str, action_code: str, line: int =
|
|
|
868
890
|
|
|
869
891
|
if lang == "python":
|
|
870
892
|
try:
|
|
871
|
-
result =
|
|
893
|
+
result = await async_execute(
|
|
872
894
|
["ruff", "check", str(path), "--fix", "--select", action_code],
|
|
873
|
-
capture_output=True,
|
|
874
|
-
text=True,
|
|
875
895
|
timeout=15,
|
|
876
896
|
)
|
|
877
897
|
|
|
@@ -885,7 +905,7 @@ async def lsp_code_action_resolve(file_path: str, action_code: str, line: int =
|
|
|
885
905
|
|
|
886
906
|
except FileNotFoundError:
|
|
887
907
|
return "Install ruff: pip install ruff"
|
|
888
|
-
except
|
|
908
|
+
except asyncio.TimeoutError:
|
|
889
909
|
return "Timeout applying fix"
|
|
890
910
|
except Exception as e:
|
|
891
911
|
return f"Error: {str(e)}"
|
|
@@ -956,6 +976,10 @@ async def lsp_servers() -> str:
|
|
|
956
976
|
# USER-VISIBLE NOTIFICATION
|
|
957
977
|
print("🖥️ LSP-SERVERS: listing installed servers", file=sys.stderr)
|
|
958
978
|
|
|
979
|
+
# Check env var overrides
|
|
980
|
+
py_cmd = os.environ.get("LSP_CMD_PYTHON", "jedi-language-server")
|
|
981
|
+
ts_cmd = os.environ.get("LSP_CMD_TYPESCRIPT", "typescript-language-server")
|
|
982
|
+
|
|
959
983
|
servers = [
|
|
960
984
|
("python", "jedi", "pip install jedi"),
|
|
961
985
|
("python", "jedi-language-server", "pip install jedi-language-server"),
|
|
@@ -965,12 +989,21 @@ async def lsp_servers() -> str:
|
|
|
965
989
|
("rust", "rust-analyzer", "rustup component add rust-analyzer"),
|
|
966
990
|
]
|
|
967
991
|
|
|
968
|
-
lines = [
|
|
992
|
+
lines = [
|
|
993
|
+
"**LSP Configuration (Env Vars):**",
|
|
994
|
+
f"- `LSP_CMD_PYTHON`: `{py_cmd}`",
|
|
995
|
+
f"- `LSP_CMD_TYPESCRIPT`: `{ts_cmd}`",
|
|
996
|
+
"",
|
|
997
|
+
"**Installation Status:**",
|
|
998
|
+
"| Language | Server | Status | Install |",
|
|
999
|
+
"|----------|--------|--------|---------|",
|
|
1000
|
+
]
|
|
969
1001
|
|
|
970
1002
|
for lang, server, install in servers:
|
|
971
1003
|
# Check if installed
|
|
972
1004
|
try:
|
|
973
|
-
|
|
1005
|
+
cmd = server.split()[0] # simple check for command
|
|
1006
|
+
await async_execute([cmd, "--version"], timeout=2)
|
|
974
1007
|
status = "✅ Installed"
|
|
975
1008
|
except FileNotFoundError:
|
|
976
1009
|
status = "❌ Not installed"
|