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.

Files changed (184) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/__init__.py +16 -6
  3. mcp_bridge/auth/cli.py +202 -11
  4. mcp_bridge/auth/oauth.py +1 -2
  5. mcp_bridge/auth/openai_oauth.py +4 -7
  6. mcp_bridge/auth/token_store.py +0 -1
  7. mcp_bridge/cli/__init__.py +1 -1
  8. mcp_bridge/cli/install_hooks.py +503 -107
  9. mcp_bridge/cli/session_report.py +0 -3
  10. mcp_bridge/config/__init__.py +2 -2
  11. mcp_bridge/config/hook_config.py +3 -5
  12. mcp_bridge/config/rate_limits.py +108 -13
  13. mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
  14. mcp_bridge/hooks/__init__.py +14 -4
  15. mcp_bridge/hooks/agent_reminder.py +4 -4
  16. mcp_bridge/hooks/auto_slash_command.py +5 -5
  17. mcp_bridge/hooks/budget_optimizer.py +2 -2
  18. mcp_bridge/hooks/claude_limits_hook.py +114 -0
  19. mcp_bridge/hooks/comment_checker.py +3 -4
  20. mcp_bridge/hooks/compaction.py +2 -2
  21. mcp_bridge/hooks/context.py +2 -1
  22. mcp_bridge/hooks/context_monitor.py +2 -2
  23. mcp_bridge/hooks/delegation_policy.py +85 -0
  24. mcp_bridge/hooks/directory_context.py +3 -3
  25. mcp_bridge/hooks/edit_recovery.py +3 -2
  26. mcp_bridge/hooks/edit_recovery_policy.py +49 -0
  27. mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
  28. mcp_bridge/hooks/events.py +160 -0
  29. mcp_bridge/hooks/git_noninteractive.py +4 -4
  30. mcp_bridge/hooks/keyword_detector.py +8 -10
  31. mcp_bridge/hooks/manager.py +35 -22
  32. mcp_bridge/hooks/notification_hook.py +13 -6
  33. mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
  34. mcp_bridge/hooks/parallel_enforcer.py +5 -5
  35. mcp_bridge/hooks/parallel_execution.py +22 -10
  36. mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
  37. mcp_bridge/hooks/pre_compact.py +8 -9
  38. mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
  39. mcp_bridge/hooks/preemptive_compaction.py +2 -3
  40. mcp_bridge/hooks/routing_notifications.py +80 -0
  41. mcp_bridge/hooks/rules_injector.py +11 -19
  42. mcp_bridge/hooks/session_idle.py +4 -4
  43. mcp_bridge/hooks/session_notifier.py +4 -4
  44. mcp_bridge/hooks/session_recovery.py +4 -5
  45. mcp_bridge/hooks/stravinsky_mode.py +1 -1
  46. mcp_bridge/hooks/subagent_stop.py +1 -3
  47. mcp_bridge/hooks/task_validator.py +2 -2
  48. mcp_bridge/hooks/tmux_manager.py +7 -8
  49. mcp_bridge/hooks/todo_delegation.py +4 -1
  50. mcp_bridge/hooks/todo_enforcer.py +180 -10
  51. mcp_bridge/hooks/truncation_policy.py +37 -0
  52. mcp_bridge/hooks/truncator.py +1 -2
  53. mcp_bridge/metrics/cost_tracker.py +115 -0
  54. mcp_bridge/native_search.py +93 -0
  55. mcp_bridge/native_watcher.py +118 -0
  56. mcp_bridge/notifications.py +3 -4
  57. mcp_bridge/orchestrator/enums.py +11 -0
  58. mcp_bridge/orchestrator/router.py +165 -0
  59. mcp_bridge/orchestrator/state.py +32 -0
  60. mcp_bridge/orchestrator/visualization.py +14 -0
  61. mcp_bridge/orchestrator/wisdom.py +34 -0
  62. mcp_bridge/prompts/__init__.py +1 -8
  63. mcp_bridge/prompts/dewey.py +1 -1
  64. mcp_bridge/prompts/planner.py +2 -4
  65. mcp_bridge/prompts/stravinsky.py +53 -31
  66. mcp_bridge/proxy/__init__.py +0 -0
  67. mcp_bridge/proxy/client.py +70 -0
  68. mcp_bridge/proxy/model_server.py +157 -0
  69. mcp_bridge/routing/__init__.py +43 -0
  70. mcp_bridge/routing/config.py +250 -0
  71. mcp_bridge/routing/model_tiers.py +135 -0
  72. mcp_bridge/routing/provider_state.py +261 -0
  73. mcp_bridge/routing/task_classifier.py +190 -0
  74. mcp_bridge/server.py +363 -34
  75. mcp_bridge/server_tools.py +298 -6
  76. mcp_bridge/tools/__init__.py +19 -8
  77. mcp_bridge/tools/agent_manager.py +549 -799
  78. mcp_bridge/tools/background_tasks.py +13 -17
  79. mcp_bridge/tools/code_search.py +54 -51
  80. mcp_bridge/tools/continuous_loop.py +0 -1
  81. mcp_bridge/tools/dashboard.py +19 -0
  82. mcp_bridge/tools/find_code.py +296 -0
  83. mcp_bridge/tools/init.py +1 -0
  84. mcp_bridge/tools/list_directory.py +42 -0
  85. mcp_bridge/tools/lsp/__init__.py +8 -8
  86. mcp_bridge/tools/lsp/manager.py +51 -28
  87. mcp_bridge/tools/lsp/tools.py +98 -65
  88. mcp_bridge/tools/model_invoke.py +1047 -152
  89. mcp_bridge/tools/mux_client.py +75 -0
  90. mcp_bridge/tools/project_context.py +1 -2
  91. mcp_bridge/tools/query_classifier.py +132 -49
  92. mcp_bridge/tools/read_file.py +84 -0
  93. mcp_bridge/tools/replace.py +45 -0
  94. mcp_bridge/tools/run_shell_command.py +38 -0
  95. mcp_bridge/tools/search_enhancements.py +347 -0
  96. mcp_bridge/tools/semantic_search.py +677 -92
  97. mcp_bridge/tools/session_manager.py +0 -2
  98. mcp_bridge/tools/skill_loader.py +0 -1
  99. mcp_bridge/tools/task_runner.py +5 -7
  100. mcp_bridge/tools/templates.py +3 -3
  101. mcp_bridge/tools/tool_search.py +331 -0
  102. mcp_bridge/tools/write_file.py +29 -0
  103. mcp_bridge/update_manager.py +33 -37
  104. mcp_bridge/update_manager_pypi.py +6 -8
  105. mcp_bridge/utils/cache.py +82 -0
  106. mcp_bridge/utils/process.py +71 -0
  107. mcp_bridge/utils/session_state.py +51 -0
  108. mcp_bridge/utils/truncation.py +76 -0
  109. {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/METADATA +84 -35
  110. stravinsky-0.4.66.dist-info/RECORD +198 -0
  111. {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
  112. stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
  113. stravinsky_claude_assets/agents/HOOKS.md +437 -0
  114. stravinsky_claude_assets/agents/code-reviewer.md +210 -0
  115. stravinsky_claude_assets/agents/comment_checker.md +580 -0
  116. stravinsky_claude_assets/agents/debugger.md +254 -0
  117. stravinsky_claude_assets/agents/delphi.md +495 -0
  118. stravinsky_claude_assets/agents/dewey.md +248 -0
  119. stravinsky_claude_assets/agents/explore.md +1198 -0
  120. stravinsky_claude_assets/agents/frontend.md +472 -0
  121. stravinsky_claude_assets/agents/implementation-lead.md +164 -0
  122. stravinsky_claude_assets/agents/momus.md +464 -0
  123. stravinsky_claude_assets/agents/research-lead.md +141 -0
  124. stravinsky_claude_assets/agents/stravinsky.md +730 -0
  125. stravinsky_claude_assets/commands/delphi.md +9 -0
  126. stravinsky_claude_assets/commands/dewey.md +54 -0
  127. stravinsky_claude_assets/commands/git-master.md +112 -0
  128. stravinsky_claude_assets/commands/index.md +49 -0
  129. stravinsky_claude_assets/commands/publish.md +86 -0
  130. stravinsky_claude_assets/commands/review.md +73 -0
  131. stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
  132. stravinsky_claude_assets/commands/str/agent_list.md +56 -0
  133. stravinsky_claude_assets/commands/str/agent_output.md +92 -0
  134. stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
  135. stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
  136. stravinsky_claude_assets/commands/str/cancel.md +51 -0
  137. stravinsky_claude_assets/commands/str/clean.md +97 -0
  138. stravinsky_claude_assets/commands/str/continue.md +38 -0
  139. stravinsky_claude_assets/commands/str/index.md +199 -0
  140. stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
  141. stravinsky_claude_assets/commands/str/search.md +205 -0
  142. stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
  143. stravinsky_claude_assets/commands/str/stats.md +71 -0
  144. stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
  145. stravinsky_claude_assets/commands/str/unwatch.md +42 -0
  146. stravinsky_claude_assets/commands/str/watch.md +45 -0
  147. stravinsky_claude_assets/commands/strav.md +53 -0
  148. stravinsky_claude_assets/commands/stravinsky.md +292 -0
  149. stravinsky_claude_assets/commands/verify.md +60 -0
  150. stravinsky_claude_assets/commands/version.md +5 -0
  151. stravinsky_claude_assets/hooks/README.md +248 -0
  152. stravinsky_claude_assets/hooks/comment_checker.py +193 -0
  153. stravinsky_claude_assets/hooks/context.py +38 -0
  154. stravinsky_claude_assets/hooks/context_monitor.py +153 -0
  155. stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
  156. stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
  157. stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
  158. stravinsky_claude_assets/hooks/notification_hook.py +103 -0
  159. stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
  160. stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
  161. stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
  162. stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
  163. stravinsky_claude_assets/hooks/pre_compact.py +123 -0
  164. stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
  165. stravinsky_claude_assets/hooks/session_recovery.py +263 -0
  166. stravinsky_claude_assets/hooks/stop_hook.py +89 -0
  167. stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
  168. stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
  169. stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
  170. stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
  171. stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
  172. stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
  173. stravinsky_claude_assets/hooks/truncator.py +23 -0
  174. stravinsky_claude_assets/rules/deployment_safety.md +51 -0
  175. stravinsky_claude_assets/rules/integration_wiring.md +89 -0
  176. stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
  177. stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
  178. stravinsky_claude_assets/settings.json +152 -0
  179. stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
  180. stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
  181. stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
  182. stravinsky_claude_assets/task_dependencies.json +34 -0
  183. stravinsky-0.4.18.dist-info/RECORD +0 -88
  184. {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
@@ -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
- lsp_hover,
9
- lsp_goto_definition,
10
- lsp_find_references,
9
+ lsp_code_action_resolve,
10
+ lsp_code_actions,
11
11
  lsp_document_symbols,
12
- lsp_workspace_symbols,
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",
@@ -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 pathlib import Path
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
- WorkspaceFolder,
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: Optional[JsonRPCClient] = None
46
+ client: JsonRPCClient | None = None
52
47
  initialized: bool = False
53
- process: Optional[asyncio.subprocess.Process] = None
54
- pid: Optional[int] = None # Track subprocess PID for explicit cleanup
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: Optional[asyncio.Task] = None
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
- self._servers["python"] = LSPServer(name="python", command=["jedi-language-server"])
93
- self._servers["typescript"] = LSPServer(
94
- name="typescript", command=["typescript-language-server", "--stdio"]
95
- )
96
-
97
- async def get_server(self, language: str) -> Optional[JsonRPCClient]:
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
- await client.start_io(server.command[0], *server.command[1:])
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=None, capabilities=ClientCapabilities()
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 asyncio.TimeoutError:
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 asyncio.TimeoutError:
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 asyncio.TimeoutError:
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 asyncio.TimeoutError:
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: Optional[LSPManager] = None
459
+ _manager_instance: LSPManager | None = None
437
460
  _manager_lock = threading.Lock()
438
461
 
439
462
 
@@ -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 subprocess
11
+ import os
12
12
  import sys
13
13
  from pathlib import Path
14
- from typing import Any, Dict, List, Optional, Tuple, Union
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
- ) -> Tuple[Optional[Any], Optional[str], str]:
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 = subprocess.run(
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
- logger.info(f"Type: {{c.type}}")
161
- logger.info(f"Name: {{c.full_name}}")
202
+ print(f"Type: {{c.type}}")
203
+ print(f"Name: {{c.full_name}}")
162
204
  if c.docstring():
163
- logger.info(f"\\nDocstring:\\n{{c.docstring()[:500]}}")
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 f"TypeScript hover requires running language server. Use Claude Code's native hover."
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 subprocess.TimeoutExpired:
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 = subprocess.run(
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
- logger.info(f"{{d.module_path}}:{{d.line}}:{{d.column}} - {{d.full_name}}")
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 as e:
271
- return f"Tool not found: Install jedi: pip install jedi"
272
- except subprocess.TimeoutExpired:
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 = subprocess.run(
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
- logger.info(f"{{r.module_path}}:{{r.line}}:{{r.column}}")
378
+ print(f"{{r.module_path}}:{{r.line}}:{{r.column}}")
341
379
  if len(references) > 30:
342
- logger.info(f"... and {{len(references) - 30}} more")
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 subprocess.TimeoutExpired:
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 = subprocess.run(
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
- logger.info(f"{{n.line:4d}} | {{indent}}{{n.type:10}} {{n.name}}")
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 = subprocess.run(
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 subprocess.TimeoutExpired:
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 = subprocess.run(
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 = subprocess.run(
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 subprocess.TimeoutExpired:
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 = subprocess.run(
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
- logger.info(f"Symbol: {{refs[0].name}}")
600
- logger.info(f"Type: {{refs[0].type}}")
601
- logger.info(f"References: {{len(refs)}}")
602
- logger.info("✅ Rename is valid")
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
- logger.info("❌ No symbol found at position")
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 = subprocess.run(
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
- logger.info(f"File: {{path}}")
702
- logger.info(changed[:500])
703
- logger.info("---")
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: Dict[str, List[Any]]):
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 = subprocess.run(
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 = subprocess.run(
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 subprocess.TimeoutExpired:
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 = ["| Language | Server | Status | Install |", "|----------|--------|--------|---------|"]
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
- subprocess.run([server, "--version"], capture_output=True, timeout=2)
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"