stravinsky 0.2.67__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 (190) 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 +112 -11
  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/MANIFEST_SCHEMA.md +305 -0
  11. mcp_bridge/config/README.md +276 -0
  12. mcp_bridge/config/__init__.py +2 -2
  13. mcp_bridge/config/hook_config.py +247 -0
  14. mcp_bridge/config/hooks_manifest.json +138 -0
  15. mcp_bridge/config/rate_limits.py +317 -0
  16. mcp_bridge/config/skills_manifest.json +128 -0
  17. mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
  18. mcp_bridge/hooks/__init__.py +19 -4
  19. mcp_bridge/hooks/agent_reminder.py +4 -4
  20. mcp_bridge/hooks/auto_slash_command.py +5 -5
  21. mcp_bridge/hooks/budget_optimizer.py +2 -2
  22. mcp_bridge/hooks/claude_limits_hook.py +114 -0
  23. mcp_bridge/hooks/comment_checker.py +3 -4
  24. mcp_bridge/hooks/compaction.py +2 -2
  25. mcp_bridge/hooks/context.py +2 -1
  26. mcp_bridge/hooks/context_monitor.py +2 -2
  27. mcp_bridge/hooks/delegation_policy.py +85 -0
  28. mcp_bridge/hooks/directory_context.py +3 -3
  29. mcp_bridge/hooks/edit_recovery.py +3 -2
  30. mcp_bridge/hooks/edit_recovery_policy.py +49 -0
  31. mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
  32. mcp_bridge/hooks/events.py +160 -0
  33. mcp_bridge/hooks/git_noninteractive.py +4 -4
  34. mcp_bridge/hooks/keyword_detector.py +8 -10
  35. mcp_bridge/hooks/manager.py +43 -22
  36. mcp_bridge/hooks/notification_hook.py +13 -6
  37. mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
  38. mcp_bridge/hooks/parallel_enforcer.py +5 -5
  39. mcp_bridge/hooks/parallel_execution.py +22 -10
  40. mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
  41. mcp_bridge/hooks/pre_compact.py +8 -9
  42. mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
  43. mcp_bridge/hooks/preemptive_compaction.py +2 -3
  44. mcp_bridge/hooks/routing_notifications.py +80 -0
  45. mcp_bridge/hooks/rules_injector.py +11 -19
  46. mcp_bridge/hooks/session_idle.py +4 -4
  47. mcp_bridge/hooks/session_notifier.py +4 -4
  48. mcp_bridge/hooks/session_recovery.py +4 -5
  49. mcp_bridge/hooks/stravinsky_mode.py +1 -1
  50. mcp_bridge/hooks/subagent_stop.py +1 -3
  51. mcp_bridge/hooks/task_validator.py +2 -2
  52. mcp_bridge/hooks/tmux_manager.py +7 -8
  53. mcp_bridge/hooks/todo_delegation.py +4 -1
  54. mcp_bridge/hooks/todo_enforcer.py +180 -10
  55. mcp_bridge/hooks/tool_messaging.py +113 -10
  56. mcp_bridge/hooks/truncation_policy.py +37 -0
  57. mcp_bridge/hooks/truncator.py +1 -2
  58. mcp_bridge/metrics/cost_tracker.py +115 -0
  59. mcp_bridge/native_search.py +93 -0
  60. mcp_bridge/native_watcher.py +118 -0
  61. mcp_bridge/notifications.py +150 -0
  62. mcp_bridge/orchestrator/enums.py +11 -0
  63. mcp_bridge/orchestrator/router.py +165 -0
  64. mcp_bridge/orchestrator/state.py +32 -0
  65. mcp_bridge/orchestrator/visualization.py +14 -0
  66. mcp_bridge/orchestrator/wisdom.py +34 -0
  67. mcp_bridge/prompts/__init__.py +1 -8
  68. mcp_bridge/prompts/dewey.py +1 -1
  69. mcp_bridge/prompts/planner.py +2 -4
  70. mcp_bridge/prompts/stravinsky.py +53 -31
  71. mcp_bridge/proxy/__init__.py +0 -0
  72. mcp_bridge/proxy/client.py +70 -0
  73. mcp_bridge/proxy/model_server.py +157 -0
  74. mcp_bridge/routing/__init__.py +43 -0
  75. mcp_bridge/routing/config.py +250 -0
  76. mcp_bridge/routing/model_tiers.py +135 -0
  77. mcp_bridge/routing/provider_state.py +261 -0
  78. mcp_bridge/routing/task_classifier.py +190 -0
  79. mcp_bridge/server.py +542 -59
  80. mcp_bridge/server_tools.py +738 -6
  81. mcp_bridge/tools/__init__.py +40 -25
  82. mcp_bridge/tools/agent_manager.py +616 -697
  83. mcp_bridge/tools/background_tasks.py +13 -17
  84. mcp_bridge/tools/code_search.py +70 -53
  85. mcp_bridge/tools/continuous_loop.py +0 -1
  86. mcp_bridge/tools/dashboard.py +19 -0
  87. mcp_bridge/tools/find_code.py +296 -0
  88. mcp_bridge/tools/init.py +1 -0
  89. mcp_bridge/tools/list_directory.py +42 -0
  90. mcp_bridge/tools/lsp/__init__.py +12 -5
  91. mcp_bridge/tools/lsp/manager.py +471 -0
  92. mcp_bridge/tools/lsp/tools.py +723 -207
  93. mcp_bridge/tools/model_invoke.py +1195 -273
  94. mcp_bridge/tools/mux_client.py +75 -0
  95. mcp_bridge/tools/project_context.py +1 -2
  96. mcp_bridge/tools/query_classifier.py +406 -0
  97. mcp_bridge/tools/read_file.py +84 -0
  98. mcp_bridge/tools/replace.py +45 -0
  99. mcp_bridge/tools/run_shell_command.py +38 -0
  100. mcp_bridge/tools/search_enhancements.py +347 -0
  101. mcp_bridge/tools/semantic_search.py +3627 -0
  102. mcp_bridge/tools/session_manager.py +0 -2
  103. mcp_bridge/tools/skill_loader.py +0 -1
  104. mcp_bridge/tools/task_runner.py +5 -7
  105. mcp_bridge/tools/templates.py +3 -3
  106. mcp_bridge/tools/tool_search.py +331 -0
  107. mcp_bridge/tools/write_file.py +29 -0
  108. mcp_bridge/update_manager.py +585 -0
  109. mcp_bridge/update_manager_pypi.py +297 -0
  110. mcp_bridge/utils/cache.py +82 -0
  111. mcp_bridge/utils/process.py +71 -0
  112. mcp_bridge/utils/session_state.py +51 -0
  113. mcp_bridge/utils/truncation.py +76 -0
  114. stravinsky-0.4.66.dist-info/METADATA +517 -0
  115. stravinsky-0.4.66.dist-info/RECORD +198 -0
  116. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
  117. stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
  118. stravinsky_claude_assets/agents/HOOKS.md +437 -0
  119. stravinsky_claude_assets/agents/code-reviewer.md +210 -0
  120. stravinsky_claude_assets/agents/comment_checker.md +580 -0
  121. stravinsky_claude_assets/agents/debugger.md +254 -0
  122. stravinsky_claude_assets/agents/delphi.md +495 -0
  123. stravinsky_claude_assets/agents/dewey.md +248 -0
  124. stravinsky_claude_assets/agents/explore.md +1198 -0
  125. stravinsky_claude_assets/agents/frontend.md +472 -0
  126. stravinsky_claude_assets/agents/implementation-lead.md +164 -0
  127. stravinsky_claude_assets/agents/momus.md +464 -0
  128. stravinsky_claude_assets/agents/research-lead.md +141 -0
  129. stravinsky_claude_assets/agents/stravinsky.md +730 -0
  130. stravinsky_claude_assets/commands/delphi.md +9 -0
  131. stravinsky_claude_assets/commands/dewey.md +54 -0
  132. stravinsky_claude_assets/commands/git-master.md +112 -0
  133. stravinsky_claude_assets/commands/index.md +49 -0
  134. stravinsky_claude_assets/commands/publish.md +86 -0
  135. stravinsky_claude_assets/commands/review.md +73 -0
  136. stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
  137. stravinsky_claude_assets/commands/str/agent_list.md +56 -0
  138. stravinsky_claude_assets/commands/str/agent_output.md +92 -0
  139. stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
  140. stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
  141. stravinsky_claude_assets/commands/str/cancel.md +51 -0
  142. stravinsky_claude_assets/commands/str/clean.md +97 -0
  143. stravinsky_claude_assets/commands/str/continue.md +38 -0
  144. stravinsky_claude_assets/commands/str/index.md +199 -0
  145. stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
  146. stravinsky_claude_assets/commands/str/search.md +205 -0
  147. stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
  148. stravinsky_claude_assets/commands/str/stats.md +71 -0
  149. stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
  150. stravinsky_claude_assets/commands/str/unwatch.md +42 -0
  151. stravinsky_claude_assets/commands/str/watch.md +45 -0
  152. stravinsky_claude_assets/commands/strav.md +53 -0
  153. stravinsky_claude_assets/commands/stravinsky.md +292 -0
  154. stravinsky_claude_assets/commands/verify.md +60 -0
  155. stravinsky_claude_assets/commands/version.md +5 -0
  156. stravinsky_claude_assets/hooks/README.md +248 -0
  157. stravinsky_claude_assets/hooks/comment_checker.py +193 -0
  158. stravinsky_claude_assets/hooks/context.py +38 -0
  159. stravinsky_claude_assets/hooks/context_monitor.py +153 -0
  160. stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
  161. stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
  162. stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
  163. stravinsky_claude_assets/hooks/notification_hook.py +103 -0
  164. stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
  165. stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
  166. stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
  167. stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
  168. stravinsky_claude_assets/hooks/pre_compact.py +123 -0
  169. stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
  170. stravinsky_claude_assets/hooks/session_recovery.py +263 -0
  171. stravinsky_claude_assets/hooks/stop_hook.py +89 -0
  172. stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
  173. stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
  174. stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
  175. stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
  176. stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
  177. stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
  178. stravinsky_claude_assets/hooks/truncator.py +23 -0
  179. stravinsky_claude_assets/rules/deployment_safety.md +51 -0
  180. stravinsky_claude_assets/rules/integration_wiring.md +89 -0
  181. stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
  182. stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
  183. stravinsky_claude_assets/settings.json +152 -0
  184. stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
  185. stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
  186. stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
  187. stravinsky_claude_assets/task_dependencies.json +34 -0
  188. stravinsky-0.2.67.dist-info/METADATA +0 -284
  189. stravinsky-0.2.67.dist-info/RECORD +0 -76
  190. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
mcp_bridge/server.py CHANGED
@@ -7,22 +7,21 @@ Optimized for extremely fast startup and protocol compliance:
7
7
  - Robust crash logging to stderr and /tmp.
8
8
  """
9
9
 
10
- import sys
11
- import os
12
10
  import asyncio
13
11
  import logging
12
+ import os
13
+ import sys
14
14
  import time
15
15
  from typing import Any
16
16
 
17
17
  from mcp.server import Server
18
18
  from mcp.server.stdio import stdio_server
19
19
  from mcp.types import (
20
- Tool,
21
- TextContent,
22
- Resource,
20
+ GetPromptResult,
23
21
  Prompt,
24
22
  PromptMessage,
25
- GetPromptResult,
23
+ TextContent,
24
+ Tool,
26
25
  )
27
26
 
28
27
  from . import __version__
@@ -35,6 +34,26 @@ logging.basicConfig(
35
34
  )
36
35
  logger = logging.getLogger(__name__)
37
36
 
37
+ # --- LOAD .env FILES (GEMINI_API_KEY, etc.) ---
38
+ # Load from ~/.stravinsky/.env (dedicated config location)
39
+ try:
40
+ from pathlib import Path
41
+
42
+ from dotenv import load_dotenv
43
+
44
+ # Load from ~/.env (user-global, lowest priority)
45
+ home_env = Path.home() / ".env"
46
+ if home_env.exists():
47
+ load_dotenv(home_env, override=False)
48
+
49
+ # Load from ~/.stravinsky/.env (stravinsky config, takes precedence)
50
+ stravinsky_env = Path.home() / ".stravinsky" / ".env"
51
+ if stravinsky_env.exists():
52
+ load_dotenv(stravinsky_env, override=True)
53
+ logger.info(f"[Config] Loaded environment from {stravinsky_env}")
54
+ except ImportError:
55
+ pass # python-dotenv not installed, skip
56
+
38
57
 
39
58
  # Pre-async crash logger
40
59
  def install_emergency_logger():
@@ -95,56 +114,9 @@ async def list_tools() -> list[Tool]:
95
114
  return get_tool_definitions()
96
115
 
97
116
 
98
- def _format_tool_log(name: str, arguments: dict[str, Any]) -> str:
99
- """Format a concise log message for tool calls."""
100
- # LSP tools - show file:line
101
- if name.startswith("lsp_"):
102
- file_path = arguments.get("file_path", "")
103
- if file_path:
104
- # Shorten path to last 2 components
105
- parts = file_path.split("/")
106
- short_path = "/".join(parts[-2:]) if len(parts) > 2 else file_path
107
- line = arguments.get("line", "")
108
- if line:
109
- return f"→ {name}: {short_path}:{line}"
110
- return f"→ {name}: {short_path}"
111
- query = arguments.get("query", "")
112
- if query:
113
- return f"→ {name}: query='{query[:40]}'"
114
- return f"→ {name}"
115
-
116
- # Model invocation - show agent context if present
117
- if name in ("invoke_gemini", "invoke_openai"):
118
- agent_ctx = arguments.get("agent_context", {})
119
- agent_type = agent_ctx.get("agent_type", "direct") if agent_ctx else "direct"
120
- model = arguments.get("model", "default")
121
- prompt = arguments.get("prompt", "")
122
- # Summarize prompt
123
- summary = " ".join(prompt.split())[:80] + "..." if len(prompt) > 80 else prompt
124
- return f"[{agent_type}] → {model}: {summary}"
125
-
126
- # Search tools - show pattern
127
- if name in ("grep_search", "ast_grep_search", "ast_grep_replace"):
128
- pattern = arguments.get("pattern", "")[:50]
129
- return f"→ {name}: pattern='{pattern}'"
130
-
131
- # Agent tools - show agent type/task_id
132
- if name == "agent_spawn":
133
- agent_type = arguments.get("agent_type", "explore")
134
- desc = arguments.get("description", "")[:40]
135
- return f"→ {name}: [{agent_type}] {desc}"
136
- if name in ("agent_output", "agent_cancel", "agent_progress"):
137
- task_id = arguments.get("task_id", "")
138
- return f"→ {name}: {task_id}"
139
-
140
- # Default - just tool name
141
- return f"→ {name}"
142
-
143
-
144
117
  @server.call_tool()
145
118
  async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
146
119
  """Handle tool calls with deep lazy loading of implementations."""
147
- logger.info(_format_tool_log(name, arguments))
148
120
  hook_manager = get_hook_manager_lazy()
149
121
  token_store = get_token_store()
150
122
 
@@ -167,6 +139,17 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
167
139
  thinking_budget=arguments.get("thinking_budget", 0),
168
140
  )
169
141
 
142
+ elif name == "invoke_gemini_agentic":
143
+ from .tools.model_invoke import invoke_gemini_agentic
144
+
145
+ result_content = await invoke_gemini_agentic(
146
+ token_store=token_store,
147
+ prompt=arguments["prompt"],
148
+ model=arguments.get("model", "gemini-3-flash"),
149
+ max_turns=arguments.get("max_turns", 10),
150
+ timeout=arguments.get("timeout", 120),
151
+ )
152
+
170
153
  elif name == "invoke_openai":
171
154
  from .tools.model_invoke import invoke_openai
172
155
 
@@ -177,6 +160,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
177
160
  temperature=arguments.get("temperature", 0.7),
178
161
  max_tokens=arguments.get("max_tokens", 4096),
179
162
  thinking_budget=arguments.get("thinking_budget", 0),
163
+ reasoning_effort=arguments.get("reasoning_effort", "medium"),
180
164
  )
181
165
 
182
166
  # --- CONTEXT DISPATCH ---
@@ -190,6 +174,19 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
190
174
 
191
175
  result_content = await get_system_health()
192
176
 
177
+ elif name == "semantic_health":
178
+ from .tools.semantic_search import semantic_health
179
+
180
+ result_content = await semantic_health(
181
+ project_path=arguments.get("project_path", "."),
182
+ provider=arguments.get("provider", "ollama"),
183
+ )
184
+
185
+ elif name == "lsp_health":
186
+ from .tools.lsp.tools import lsp_health
187
+
188
+ result_content = await lsp_health()
189
+
193
190
  # --- SEARCH DISPATCH ---
194
191
  elif name == "grep_search":
195
192
  from .tools.code_search import grep_search
@@ -200,6 +197,13 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
200
197
  file_pattern=arguments.get("file_pattern", ""),
201
198
  )
202
199
 
200
+ elif name == "list_directory":
201
+ from .tools.list_directory import list_directory
202
+
203
+ result_content = await list_directory(
204
+ path=arguments["path"],
205
+ )
206
+
203
207
  elif name == "ast_grep_search":
204
208
  from .tools.code_search import ast_grep_search
205
209
 
@@ -228,6 +232,56 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
228
232
  directory=arguments.get("directory", "."),
229
233
  )
230
234
 
235
+ elif name == "read_file":
236
+ from .tools.read_file import read_file
237
+
238
+ result_content = await read_file(
239
+ path=arguments["path"],
240
+ offset=arguments.get("offset", 0),
241
+ limit=arguments.get("limit"),
242
+ )
243
+
244
+ elif name == "write_file":
245
+ from .tools.write_file import write_file
246
+
247
+ result_content = await write_file(
248
+ path=arguments["path"],
249
+ content=arguments["content"],
250
+ )
251
+
252
+ elif name == "replace":
253
+ from .tools.replace import replace
254
+
255
+ result_content = await replace(
256
+ path=arguments["path"],
257
+ old_string=arguments["old_string"],
258
+ new_string=arguments["new_string"],
259
+ instruction=arguments["instruction"],
260
+ expected_replacements=arguments.get("expected_replacements", 1),
261
+ )
262
+
263
+ elif name == "run_shell_command":
264
+ from .tools.run_shell_command import run_shell_command
265
+
266
+ result_content = await run_shell_command(
267
+ command=arguments["command"],
268
+ description=arguments["description"],
269
+ dir_path=arguments.get("dir_path", "."),
270
+ )
271
+
272
+ elif name == "tool_search":
273
+ from .tools.tool_search import search_tools
274
+ from .server_tools import get_tool_definitions
275
+
276
+ # Get all registered tool definitions to search through
277
+ all_tools = get_tool_definitions()
278
+
279
+ result_content = search_tools(
280
+ query=arguments["query"],
281
+ tools=all_tools,
282
+ top_k=arguments.get("top_k", 5),
283
+ )
284
+
231
285
  # --- SESSION DISPATCH ---
232
286
  elif name == "session_list":
233
287
  from .tools.session_manager import list_sessions
@@ -318,7 +372,15 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
318
372
  elif name == "agent_list":
319
373
  from .tools.agent_manager import agent_list
320
374
 
321
- result_content = await agent_list()
375
+ result_content = await agent_list(show_all=arguments.get("show_all", True))
376
+
377
+ elif name == "agent_cleanup":
378
+ from .tools.agent_manager import agent_cleanup
379
+
380
+ result_content = await agent_cleanup(
381
+ max_age_minutes=arguments.get("max_age_minutes", 30),
382
+ statuses=arguments.get("statuses"),
383
+ )
322
384
 
323
385
  elif name == "agent_progress":
324
386
  from .tools.agent_manager import agent_progress
@@ -423,6 +485,28 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
423
485
  character=arguments["character"],
424
486
  )
425
487
 
488
+ elif name == "lsp_code_action_resolve":
489
+ from .tools.lsp import lsp_code_action_resolve
490
+
491
+ result_content = await lsp_code_action_resolve(
492
+ file_path=arguments["file_path"],
493
+ action_code=arguments["action_code"],
494
+ line=arguments.get("line"),
495
+ )
496
+
497
+ elif name == "lsp_extract_refactor":
498
+ from .tools.lsp import lsp_extract_refactor
499
+
500
+ result_content = await lsp_extract_refactor(
501
+ file_path=arguments["file_path"],
502
+ start_line=arguments["start_line"],
503
+ start_char=arguments["start_char"],
504
+ end_line=arguments["end_line"],
505
+ end_char=arguments["end_char"],
506
+ new_name=arguments["new_name"],
507
+ kind=arguments.get("kind", "function"),
508
+ )
509
+
426
510
  elif name == "lsp_servers":
427
511
  from .tools.lsp import lsp_servers
428
512
 
@@ -436,6 +520,222 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
436
520
  severity=arguments.get("severity", "all"),
437
521
  )
438
522
 
523
+ elif name == "semantic_search":
524
+ from .tools.semantic_search import semantic_search
525
+
526
+ result_content = await semantic_search(
527
+ query=arguments["query"],
528
+ project_path=arguments.get("project_path", "."),
529
+ n_results=arguments.get("n_results", 10),
530
+ language=arguments.get("language"),
531
+ node_type=arguments.get("node_type"),
532
+ provider=arguments.get("provider", "ollama"),
533
+ )
534
+
535
+ elif name == "hybrid_search":
536
+ from .tools.semantic_search import hybrid_search
537
+
538
+ result_content = await hybrid_search(
539
+ query=arguments["query"],
540
+ pattern=arguments.get("pattern"),
541
+ project_path=arguments.get("project_path", "."),
542
+ n_results=arguments.get("n_results", 10),
543
+ language=arguments.get("language"),
544
+ provider=arguments.get("provider", "ollama"),
545
+ )
546
+
547
+ elif name == "find_code":
548
+ from .tools.find_code import find_code
549
+
550
+ result_content = await find_code(
551
+ query=arguments["query"],
552
+ search_type=arguments.get("search_type", "auto"),
553
+ project_path=arguments.get("project_path", "."),
554
+ language=arguments.get("language"),
555
+ n_results=arguments.get("n_results", 10),
556
+ provider=arguments.get("provider", "ollama"),
557
+ )
558
+
559
+ elif name == "multi_query_search":
560
+ from .tools.search_enhancements import multi_query_search
561
+
562
+ result_content = await multi_query_search(
563
+ query=arguments["query"],
564
+ project_path=arguments.get("project_path", "."),
565
+ n_results=arguments.get("n_results", 10),
566
+ num_expansions=arguments.get("num_expansions", 3),
567
+ language=arguments.get("language"),
568
+ node_type=arguments.get("node_type"),
569
+ provider=arguments.get("provider", "ollama"),
570
+ )
571
+
572
+ elif name == "decomposed_search":
573
+ from .tools.search_enhancements import decomposed_search
574
+
575
+ result_content = await decomposed_search(
576
+ query=arguments["query"],
577
+ project_path=arguments.get("project_path", "."),
578
+ n_results=arguments.get("n_results", 10),
579
+ language=arguments.get("language"),
580
+ node_type=arguments.get("node_type"),
581
+ provider=arguments.get("provider", "ollama"),
582
+ )
583
+
584
+ elif name == "enhanced_search":
585
+ from .tools.search_enhancements import enhanced_search
586
+
587
+ result_content = await enhanced_search(
588
+ query=arguments["query"],
589
+ project_path=arguments.get("project_path", "."),
590
+ n_results=arguments.get("n_results", 10),
591
+ mode=arguments.get("mode", "auto"),
592
+ language=arguments.get("language"),
593
+ node_type=arguments.get("node_type"),
594
+ provider=arguments.get("provider", "ollama"),
595
+ )
596
+
597
+ elif name == "get_cost_report":
598
+ from .tools.dashboard import get_cost_report
599
+
600
+ result_content = await get_cost_report(
601
+ session_id=arguments.get("session_id"),
602
+ )
603
+
604
+ elif name == "semantic_index":
605
+ from .tools.semantic_search import index_codebase
606
+
607
+ result_content = await index_codebase(
608
+ project_path=arguments.get("project_path", "."),
609
+ force=arguments.get("force", False),
610
+ provider=arguments.get("provider", "ollama"),
611
+ )
612
+
613
+ elif name == "semantic_stats":
614
+ from .tools.semantic_search import semantic_stats
615
+
616
+ result_content = await semantic_stats(
617
+ project_path=arguments.get("project_path", "."),
618
+ provider=arguments.get("provider", "ollama"),
619
+ )
620
+
621
+ elif name == "start_file_watcher":
622
+ import json
623
+
624
+ from .tools.semantic_search import start_file_watcher
625
+
626
+ try:
627
+ watcher = await start_file_watcher(
628
+ project_path=arguments.get("project_path", "."),
629
+ provider=arguments.get("provider", "ollama"),
630
+ debounce_seconds=arguments.get("debounce_seconds", 2.0),
631
+ )
632
+
633
+ result_content = json.dumps(
634
+ {
635
+ "status": "started",
636
+ "project_path": str(watcher.project_path),
637
+ "debounce_seconds": watcher.debounce_seconds,
638
+ "provider": watcher.store.provider_name,
639
+ "is_running": watcher.is_running(),
640
+ },
641
+ indent=2,
642
+ )
643
+ except ValueError as e:
644
+ # No index exists
645
+ result_content = json.dumps(
646
+ {"error": str(e), "hint": "Run semantic_index() before starting file watcher"},
647
+ indent=2,
648
+ )
649
+ print(f"⚠️ start_file_watcher ValueError: {e}", file=sys.stderr)
650
+ except Exception as e:
651
+ # Unexpected error
652
+ import traceback
653
+
654
+ result_content = json.dumps(
655
+ {
656
+ "error": f"{type(e).__name__}: {str(e)}",
657
+ "hint": "Check MCP server logs for details",
658
+ },
659
+ indent=2,
660
+ )
661
+ print(f"❌ start_file_watcher error: {e}", file=sys.stderr)
662
+ traceback.print_exc(file=sys.stderr)
663
+
664
+ elif name == "stop_file_watcher":
665
+ import json
666
+
667
+ from .tools.semantic_search import stop_file_watcher
668
+
669
+ stopped = stop_file_watcher(
670
+ project_path=arguments.get("project_path", "."),
671
+ )
672
+
673
+ result_content = json.dumps(
674
+ {"stopped": stopped, "project_path": arguments.get("project_path", ".")}, indent=2
675
+ )
676
+
677
+ elif name == "cancel_indexing":
678
+ from .tools.semantic_search import cancel_indexing
679
+
680
+ result_content = cancel_indexing(
681
+ project_path=arguments.get("project_path", "."),
682
+ provider=arguments.get("provider", "ollama"),
683
+ )
684
+
685
+ elif name == "delete_index":
686
+ from .tools.semantic_search import delete_index
687
+
688
+ result_content = delete_index(
689
+ project_path=arguments.get("project_path", "."),
690
+ provider=arguments.get("provider"), # None if not specified
691
+ delete_all=arguments.get("delete_all", False),
692
+ )
693
+
694
+ elif name == "list_file_watchers":
695
+ import json
696
+
697
+ from .tools.semantic_search import list_file_watchers
698
+
699
+ result_content = json.dumps(list_file_watchers(), indent=2)
700
+
701
+ elif name == "multi_query_search":
702
+ from .tools.semantic_search import multi_query_search
703
+
704
+ result_content = await multi_query_search(
705
+ query=arguments["query"],
706
+ project_path=arguments.get("project_path", "."),
707
+ n_results=arguments.get("n_results", 10),
708
+ num_expansions=arguments.get("num_expansions", 3),
709
+ language=arguments.get("language"),
710
+ node_type=arguments.get("node_type"),
711
+ provider=arguments.get("provider", "ollama"),
712
+ )
713
+
714
+ elif name == "decomposed_search":
715
+ from .tools.semantic_search import decomposed_search
716
+
717
+ result_content = await decomposed_search(
718
+ query=arguments["query"],
719
+ project_path=arguments.get("project_path", "."),
720
+ n_results=arguments.get("n_results", 10),
721
+ language=arguments.get("language"),
722
+ node_type=arguments.get("node_type"),
723
+ provider=arguments.get("provider", "ollama"),
724
+ )
725
+
726
+ elif name == "enhanced_search":
727
+ from .tools.semantic_search import enhanced_search
728
+
729
+ result_content = await enhanced_search(
730
+ query=arguments["query"],
731
+ project_path=arguments.get("project_path", "."),
732
+ n_results=arguments.get("n_results", 10),
733
+ mode=arguments.get("mode", "auto"),
734
+ language=arguments.get("language"),
735
+ node_type=arguments.get("node_type"),
736
+ provider=arguments.get("provider", "ollama"),
737
+ )
738
+
439
739
  else:
440
740
  result_content = f"Unknown tool: {name}"
441
741
 
@@ -482,7 +782,7 @@ async def list_prompts() -> list[Prompt]:
482
782
  @server.get_prompt()
483
783
  async def get_prompt(name: str, arguments: dict[str, str] | None) -> GetPromptResult:
484
784
  """Get a specific prompt content (lazy loaded)."""
485
- from .prompts import stravinsky, delphi, dewey, explore, frontend, document_writer, multimodal
785
+ from .prompts import delphi, dewey, document_writer, explore, frontend, multimodal, stravinsky
486
786
 
487
787
  prompts_map = {
488
788
  "stravinsky": ("Stravinsky orchestrator system prompt", stravinsky.get_stravinsky_prompt),
@@ -511,8 +811,105 @@ async def get_prompt(name: str, arguments: dict[str, str] | None) -> GetPromptRe
511
811
  )
512
812
 
513
813
 
814
+ def sync_user_assets():
815
+ """
816
+ Copy package assets to user scope (~/.claude/) on every MCP load.
817
+
818
+ This ensures all repos get the latest commands, hooks, rules, and agents
819
+ from the installed Stravinsky package.
820
+
821
+ Handles both:
822
+ - Development: .claude/ relative to project root
823
+ - Installed package: stravinsky_claude_assets/ in site-packages
824
+ """
825
+ from pathlib import Path
826
+ import shutil
827
+
828
+ # Try multiple locations for package assets
829
+ package_dir = Path(__file__).parent.parent # stravinsky/
830
+
831
+ # Location 1: Development - .claude/ at project root
832
+ dev_claude = package_dir / ".claude"
833
+
834
+ # Location 2: Installed package - stravinsky_claude_assets in site-packages
835
+ # When installed via pip/uvx, hatch includes .claude as stravinsky_claude_assets
836
+ installed_claude = package_dir / "stravinsky_claude_assets"
837
+
838
+ # Also check relative to mcp_bridge (alternate install layout)
839
+ mcp_bridge_dir = Path(__file__).parent
840
+ installed_claude_alt = mcp_bridge_dir.parent / "stravinsky_claude_assets"
841
+
842
+ # Find the first existing assets directory
843
+ package_claude = None
844
+ for candidate in [dev_claude, installed_claude, installed_claude_alt]:
845
+ if candidate.exists():
846
+ package_claude = candidate
847
+ break
848
+
849
+ # User scope directory
850
+ user_claude = Path.home() / ".claude"
851
+
852
+ if package_claude is None:
853
+ # Try importlib.resources as last resort (Python 3.9+)
854
+ try:
855
+ import importlib.resources as resources
856
+
857
+ # Check if stravinsky_claude_assets is a package
858
+ with resources.files("stravinsky_claude_assets") as assets_path:
859
+ if assets_path.is_dir():
860
+ package_claude = Path(assets_path)
861
+ except (ImportError, ModuleNotFoundError, TypeError):
862
+ pass
863
+
864
+ if package_claude is None:
865
+ logger.debug(f"Package assets not found (checked: {dev_claude}, {installed_claude})")
866
+ return
867
+
868
+ # Directories to sync
869
+ dirs_to_sync = ["commands", "hooks", "rules", "agents"]
870
+
871
+ for dir_name in dirs_to_sync:
872
+ src_dir = package_claude / dir_name
873
+ dst_dir = user_claude / dir_name
874
+
875
+ if not src_dir.exists():
876
+ continue
877
+
878
+ # Create destination if it doesn't exist
879
+ dst_dir.mkdir(parents=True, exist_ok=True)
880
+
881
+ # Copy all files recursively (overwrite if source is newer)
882
+ for src_file in src_dir.rglob("*"):
883
+ if src_file.is_file():
884
+ # Compute relative path
885
+ rel_path = src_file.relative_to(src_dir)
886
+ dst_file = dst_dir / rel_path
887
+
888
+ # Create parent directories
889
+ dst_file.parent.mkdir(parents=True, exist_ok=True)
890
+
891
+ # Copy if source is newer or dest doesn't exist
892
+ should_copy = not dst_file.exists()
893
+ if dst_file.exists():
894
+ src_mtime = src_file.stat().st_mtime
895
+ dst_mtime = dst_file.stat().st_mtime
896
+ should_copy = src_mtime > dst_mtime
897
+
898
+ if should_copy:
899
+ shutil.copy2(src_file, dst_file)
900
+ logger.debug(f"Synced {dir_name}/{rel_path} to user scope")
901
+
902
+ logger.info("Synced package assets to user scope (~/.claude/)")
903
+
904
+
514
905
  async def async_main():
515
906
  """Server execution entry point."""
907
+ # Sync package assets to user scope on every MCP load
908
+ try:
909
+ sync_user_assets()
910
+ except Exception as e:
911
+ logger.warning(f"Failed to sync user assets: {e}")
912
+
516
913
  # Initialize hooks at runtime, not import time
517
914
  try:
518
915
  from .hooks import initialize_hooks
@@ -521,6 +918,16 @@ async def async_main():
521
918
  except Exception as e:
522
919
  logger.error(f"Failed to initialize hooks: {e}")
523
920
 
921
+ # Clean up stale ChromaDB locks on startup
922
+ try:
923
+ from .tools.semantic_search import cleanup_stale_chromadb_locks
924
+
925
+ removed_count = cleanup_stale_chromadb_locks()
926
+ if removed_count > 0:
927
+ logger.info(f"Cleaned up {removed_count} stale ChromaDB lock(s)")
928
+ except Exception as e:
929
+ logger.warning(f"Failed to cleanup ChromaDB locks: {e}")
930
+
524
931
  # Start background token refresh scheduler
525
932
  try:
526
933
  from .auth.token_refresh import background_token_refresh
@@ -537,17 +944,23 @@ async def async_main():
537
944
  write_stream,
538
945
  server.create_initialization_options(),
539
946
  )
540
- except Exception as e:
947
+ except Exception:
541
948
  logger.critical("Server process crashed in async_main", exc_info=True)
542
949
  sys.exit(1)
950
+ finally:
951
+ logger.info("Initiating shutdown sequence...")
952
+ from .tools.lsp.manager import get_lsp_manager
953
+
954
+ lsp_manager = get_lsp_manager()
955
+ await lsp_manager.shutdown()
543
956
 
544
957
 
545
958
  def main():
546
959
  """Synchronous entry point with CLI arg handling."""
547
960
  import argparse
548
- import sys
549
- from .tools.agent_manager import get_manager
961
+
550
962
  from .auth.token_store import TokenStore
963
+ from .tools.agent_manager import get_manager
551
964
 
552
965
  parser = argparse.ArgumentParser(
553
966
  description="Stravinsky MCP Bridge - Multi-model AI orchestration for Claude Code. "
@@ -597,6 +1010,31 @@ def main():
597
1010
  help="Also clear agent history from .stravinsky/agents.json",
598
1011
  )
599
1012
 
1013
+ # proxy command (model server proxy)
1014
+ proxy_parser = subparsers.add_parser(
1015
+ "proxy",
1016
+ help="Manage the model proxy server",
1017
+ description="Starts or manages the FastAPI model proxy for async execution.",
1018
+ )
1019
+ proxy_subparsers = proxy_parser.add_subparsers(
1020
+ dest="proxy_command", help="Proxy subcommands", metavar="SUBCOMMAND"
1021
+ )
1022
+ proxy_subparsers.add_parser(
1023
+ "start",
1024
+ help="Start the model proxy server",
1025
+ description="Launches the FastAPI server on port 8765.",
1026
+ )
1027
+ proxy_subparsers.add_parser(
1028
+ "stop",
1029
+ help="Stop the model proxy server",
1030
+ description="Terminates any running model proxy process.",
1031
+ )
1032
+ proxy_subparsers.add_parser(
1033
+ "status",
1034
+ help="Check proxy server status",
1035
+ description="Verifies if the proxy server is reachable.",
1036
+ )
1037
+
600
1038
  # auth command (authentication)
601
1039
  auth_parser = subparsers.add_parser(
602
1040
  "auth",
@@ -708,6 +1146,51 @@ def main():
708
1146
  print(f"Stopped {count} running agent(s).")
709
1147
  return 0
710
1148
 
1149
+ elif args.command == "proxy":
1150
+ proxy_cmd = getattr(args, "proxy_command", None)
1151
+ import os
1152
+ import sys
1153
+
1154
+ if proxy_cmd == "start":
1155
+ from .proxy.model_server import main as proxy_main
1156
+
1157
+ proxy_main()
1158
+ return 0
1159
+ elif proxy_cmd == "stop":
1160
+ # Simple kill-by-port for now
1161
+ import os
1162
+ import subprocess
1163
+
1164
+ port = int(os.getenv("STRAVINSKY_PROXY_PORT", 8765))
1165
+ try:
1166
+ if sys.platform == "win32":
1167
+ subprocess.run(
1168
+ f"taskkill /F /FI \"PID eq $(netstat -ano | findstr :{port} | awk '{{print $5}}')\"",
1169
+ shell=True,
1170
+ )
1171
+ else:
1172
+ subprocess.run(f"lsof -ti:{port} | xargs kill -9", shell=True)
1173
+ print(f"Stopped proxy on port {port}")
1174
+ except Exception as e:
1175
+ print(f"Failed to stop proxy: {e}")
1176
+ return 0
1177
+ elif proxy_cmd == "status":
1178
+ import socket
1179
+
1180
+ port = int(os.getenv("STRAVINSKY_PROXY_PORT", 8765))
1181
+ host = os.getenv("STRAVINSKY_PROXY_HOST", "127.0.0.1")
1182
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1183
+ s.settimeout(1)
1184
+ try:
1185
+ s.connect((host, port))
1186
+ print(f"✅ Proxy is RUNNING on {host}:{port}")
1187
+ except Exception:
1188
+ print(f"❌ Proxy is NOT RUNNING on {host}:{port}")
1189
+ return 0
1190
+ else:
1191
+ proxy_parser.print_help()
1192
+ return 0
1193
+
711
1194
  elif args.command == "auth":
712
1195
  auth_cmd = getattr(args, "auth_command", None)
713
1196
  token_store = get_token_store()
@@ -752,4 +1235,4 @@ def main():
752
1235
 
753
1236
 
754
1237
  if __name__ == "__main__":
755
- main()
1238
+ sys.exit(main())