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
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():
@@ -120,6 +139,17 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
120
139
  thinking_budget=arguments.get("thinking_budget", 0),
121
140
  )
122
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
+
123
153
  elif name == "invoke_openai":
124
154
  from .tools.model_invoke import invoke_openai
125
155
 
@@ -130,6 +160,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
130
160
  temperature=arguments.get("temperature", 0.7),
131
161
  max_tokens=arguments.get("max_tokens", 4096),
132
162
  thinking_budget=arguments.get("thinking_budget", 0),
163
+ reasoning_effort=arguments.get("reasoning_effort", "medium"),
133
164
  )
134
165
 
135
166
  # --- CONTEXT DISPATCH ---
@@ -166,6 +197,13 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
166
197
  file_pattern=arguments.get("file_pattern", ""),
167
198
  )
168
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
+
169
207
  elif name == "ast_grep_search":
170
208
  from .tools.code_search import ast_grep_search
171
209
 
@@ -194,6 +232,56 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
194
232
  directory=arguments.get("directory", "."),
195
233
  )
196
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
+
197
285
  # --- SESSION DISPATCH ---
198
286
  elif name == "session_list":
199
287
  from .tools.session_manager import list_sessions
@@ -284,7 +372,15 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
284
372
  elif name == "agent_list":
285
373
  from .tools.agent_manager import agent_list
286
374
 
287
- 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
+ )
288
384
 
289
385
  elif name == "agent_progress":
290
386
  from .tools.agent_manager import agent_progress
@@ -448,6 +544,63 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
448
544
  provider=arguments.get("provider", "ollama"),
449
545
  )
450
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
+
451
604
  elif name == "semantic_index":
452
605
  from .tools.semantic_search import index_codebase
453
606
 
@@ -466,52 +619,60 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
466
619
  )
467
620
 
468
621
  elif name == "start_file_watcher":
469
- from .tools.semantic_search import start_file_watcher
470
622
  import json
471
623
 
624
+ from .tools.semantic_search import start_file_watcher
625
+
472
626
  try:
473
- watcher = start_file_watcher(
627
+ watcher = await start_file_watcher(
474
628
  project_path=arguments.get("project_path", "."),
475
629
  provider=arguments.get("provider", "ollama"),
476
630
  debounce_seconds=arguments.get("debounce_seconds", 2.0),
477
631
  )
478
632
 
479
- result_content = json.dumps({
480
- "status": "started",
481
- "project_path": str(watcher.project_path),
482
- "debounce_seconds": watcher.debounce_seconds,
483
- "provider": watcher.store.provider_name,
484
- "is_running": watcher.is_running()
485
- }, indent=2)
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
+ )
486
643
  except ValueError as e:
487
644
  # No index exists
488
- result_content = json.dumps({
489
- "error": str(e),
490
- "hint": "Run semantic_index() before starting file watcher"
491
- }, indent=2)
645
+ result_content = json.dumps(
646
+ {"error": str(e), "hint": "Run semantic_index() before starting file watcher"},
647
+ indent=2,
648
+ )
492
649
  print(f"⚠️ start_file_watcher ValueError: {e}", file=sys.stderr)
493
650
  except Exception as e:
494
651
  # Unexpected error
495
652
  import traceback
496
- result_content = json.dumps({
497
- "error": f"{type(e).__name__}: {str(e)}",
498
- "hint": "Check MCP server logs for details"
499
- }, indent=2)
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
+ )
500
661
  print(f"❌ start_file_watcher error: {e}", file=sys.stderr)
501
662
  traceback.print_exc(file=sys.stderr)
502
663
 
503
664
  elif name == "stop_file_watcher":
504
- from .tools.semantic_search import stop_file_watcher
505
665
  import json
506
666
 
667
+ from .tools.semantic_search import stop_file_watcher
668
+
507
669
  stopped = stop_file_watcher(
508
670
  project_path=arguments.get("project_path", "."),
509
671
  )
510
672
 
511
- result_content = json.dumps({
512
- "stopped": stopped,
513
- "project_path": arguments.get("project_path", ".")
514
- }, indent=2)
673
+ result_content = json.dumps(
674
+ {"stopped": stopped, "project_path": arguments.get("project_path", ".")}, indent=2
675
+ )
515
676
 
516
677
  elif name == "cancel_indexing":
517
678
  from .tools.semantic_search import cancel_indexing
@@ -531,9 +692,10 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
531
692
  )
532
693
 
533
694
  elif name == "list_file_watchers":
534
- from .tools.semantic_search import list_file_watchers
535
695
  import json
536
696
 
697
+ from .tools.semantic_search import list_file_watchers
698
+
537
699
  result_content = json.dumps(list_file_watchers(), indent=2)
538
700
 
539
701
  elif name == "multi_query_search":
@@ -620,7 +782,7 @@ async def list_prompts() -> list[Prompt]:
620
782
  @server.get_prompt()
621
783
  async def get_prompt(name: str, arguments: dict[str, str] | None) -> GetPromptResult:
622
784
  """Get a specific prompt content (lazy loaded)."""
623
- from .prompts import stravinsky, delphi, dewey, explore, frontend, document_writer, multimodal
785
+ from .prompts import delphi, dewey, document_writer, explore, frontend, multimodal, stravinsky
624
786
 
625
787
  prompts_map = {
626
788
  "stravinsky": ("Stravinsky orchestrator system prompt", stravinsky.get_stravinsky_prompt),
@@ -649,8 +811,105 @@ async def get_prompt(name: str, arguments: dict[str, str] | None) -> GetPromptRe
649
811
  )
650
812
 
651
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
+
652
905
  async def async_main():
653
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
+
654
913
  # Initialize hooks at runtime, not import time
655
914
  try:
656
915
  from .hooks import initialize_hooks
@@ -685,7 +944,7 @@ async def async_main():
685
944
  write_stream,
686
945
  server.create_initialization_options(),
687
946
  )
688
- except Exception as e:
947
+ except Exception:
689
948
  logger.critical("Server process crashed in async_main", exc_info=True)
690
949
  sys.exit(1)
691
950
  finally:
@@ -699,9 +958,9 @@ async def async_main():
699
958
  def main():
700
959
  """Synchronous entry point with CLI arg handling."""
701
960
  import argparse
702
- import sys
703
- from .tools.agent_manager import get_manager
961
+
704
962
  from .auth.token_store import TokenStore
963
+ from .tools.agent_manager import get_manager
705
964
 
706
965
  parser = argparse.ArgumentParser(
707
966
  description="Stravinsky MCP Bridge - Multi-model AI orchestration for Claude Code. "
@@ -751,6 +1010,31 @@ def main():
751
1010
  help="Also clear agent history from .stravinsky/agents.json",
752
1011
  )
753
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
+
754
1038
  # auth command (authentication)
755
1039
  auth_parser = subparsers.add_parser(
756
1040
  "auth",
@@ -862,6 +1146,51 @@ def main():
862
1146
  print(f"Stopped {count} running agent(s).")
863
1147
  return 0
864
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
+
865
1194
  elif args.command == "auth":
866
1195
  auth_cmd = getattr(args, "auth_command", None)
867
1196
  token_store = get_token_store()