stravinsky 0.2.52__py3-none-any.whl → 0.4.18__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 (58) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/token_store.py +113 -11
  3. mcp_bridge/cli/__init__.py +6 -0
  4. mcp_bridge/cli/install_hooks.py +1265 -0
  5. mcp_bridge/cli/session_report.py +585 -0
  6. mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
  7. mcp_bridge/config/README.md +276 -0
  8. mcp_bridge/config/hook_config.py +249 -0
  9. mcp_bridge/config/hooks_manifest.json +138 -0
  10. mcp_bridge/config/rate_limits.py +222 -0
  11. mcp_bridge/config/skills_manifest.json +128 -0
  12. mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
  13. mcp_bridge/hooks/README.md +215 -0
  14. mcp_bridge/hooks/__init__.py +119 -60
  15. mcp_bridge/hooks/edit_recovery.py +42 -37
  16. mcp_bridge/hooks/git_noninteractive.py +89 -0
  17. mcp_bridge/hooks/keyword_detector.py +30 -0
  18. mcp_bridge/hooks/manager.py +8 -0
  19. mcp_bridge/hooks/notification_hook.py +103 -0
  20. mcp_bridge/hooks/parallel_execution.py +111 -0
  21. mcp_bridge/hooks/pre_compact.py +82 -183
  22. mcp_bridge/hooks/rules_injector.py +507 -0
  23. mcp_bridge/hooks/session_notifier.py +125 -0
  24. mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
  25. mcp_bridge/hooks/subagent_stop.py +98 -0
  26. mcp_bridge/hooks/task_validator.py +73 -0
  27. mcp_bridge/hooks/tmux_manager.py +141 -0
  28. mcp_bridge/hooks/todo_continuation.py +90 -0
  29. mcp_bridge/hooks/todo_delegation.py +88 -0
  30. mcp_bridge/hooks/tool_messaging.py +267 -0
  31. mcp_bridge/hooks/truncator.py +21 -17
  32. mcp_bridge/notifications.py +151 -0
  33. mcp_bridge/prompts/multimodal.py +24 -3
  34. mcp_bridge/server.py +214 -49
  35. mcp_bridge/server_tools.py +445 -0
  36. mcp_bridge/tools/__init__.py +22 -18
  37. mcp_bridge/tools/agent_manager.py +220 -32
  38. mcp_bridge/tools/code_search.py +97 -11
  39. mcp_bridge/tools/lsp/__init__.py +7 -0
  40. mcp_bridge/tools/lsp/manager.py +448 -0
  41. mcp_bridge/tools/lsp/tools.py +637 -150
  42. mcp_bridge/tools/model_invoke.py +208 -106
  43. mcp_bridge/tools/query_classifier.py +323 -0
  44. mcp_bridge/tools/semantic_search.py +3042 -0
  45. mcp_bridge/tools/templates.py +32 -18
  46. mcp_bridge/update_manager.py +589 -0
  47. mcp_bridge/update_manager_pypi.py +299 -0
  48. stravinsky-0.4.18.dist-info/METADATA +468 -0
  49. stravinsky-0.4.18.dist-info/RECORD +88 -0
  50. stravinsky-0.4.18.dist-info/entry_points.txt +5 -0
  51. mcp_bridge/native_hooks/edit_recovery.py +0 -46
  52. mcp_bridge/native_hooks/todo_delegation.py +0 -54
  53. mcp_bridge/native_hooks/truncator.py +0 -23
  54. stravinsky-0.2.52.dist-info/METADATA +0 -204
  55. stravinsky-0.2.52.dist-info/RECORD +0 -63
  56. stravinsky-0.2.52.dist-info/entry_points.txt +0 -3
  57. /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
  58. {stravinsky-0.2.52.dist-info → stravinsky-0.4.18.dist-info}/WHEEL +0 -0
mcp_bridge/server.py CHANGED
@@ -95,56 +95,9 @@ async def list_tools() -> list[Tool]:
95
95
  return get_tool_definitions()
96
96
 
97
97
 
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
98
  @server.call_tool()
145
99
  async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
146
100
  """Handle tool calls with deep lazy loading of implementations."""
147
- logger.info(_format_tool_log(name, arguments))
148
101
  hook_manager = get_hook_manager_lazy()
149
102
  token_store = get_token_store()
150
103
 
@@ -190,6 +143,19 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
190
143
 
191
144
  result_content = await get_system_health()
192
145
 
146
+ elif name == "semantic_health":
147
+ from .tools.semantic_search import semantic_health
148
+
149
+ result_content = await semantic_health(
150
+ project_path=arguments.get("project_path", "."),
151
+ provider=arguments.get("provider", "ollama"),
152
+ )
153
+
154
+ elif name == "lsp_health":
155
+ from .tools.lsp.tools import lsp_health
156
+
157
+ result_content = await lsp_health()
158
+
193
159
  # --- SEARCH DISPATCH ---
194
160
  elif name == "grep_search":
195
161
  from .tools.code_search import grep_search
@@ -423,11 +389,191 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
423
389
  character=arguments["character"],
424
390
  )
425
391
 
392
+ elif name == "lsp_code_action_resolve":
393
+ from .tools.lsp import lsp_code_action_resolve
394
+
395
+ result_content = await lsp_code_action_resolve(
396
+ file_path=arguments["file_path"],
397
+ action_code=arguments["action_code"],
398
+ line=arguments.get("line"),
399
+ )
400
+
401
+ elif name == "lsp_extract_refactor":
402
+ from .tools.lsp import lsp_extract_refactor
403
+
404
+ result_content = await lsp_extract_refactor(
405
+ file_path=arguments["file_path"],
406
+ start_line=arguments["start_line"],
407
+ start_char=arguments["start_char"],
408
+ end_line=arguments["end_line"],
409
+ end_char=arguments["end_char"],
410
+ new_name=arguments["new_name"],
411
+ kind=arguments.get("kind", "function"),
412
+ )
413
+
426
414
  elif name == "lsp_servers":
427
415
  from .tools.lsp import lsp_servers
428
416
 
429
417
  result_content = await lsp_servers()
430
418
 
419
+ elif name == "lsp_diagnostics":
420
+ from .tools.code_search import lsp_diagnostics
421
+
422
+ result_content = await lsp_diagnostics(
423
+ file_path=arguments["file_path"],
424
+ severity=arguments.get("severity", "all"),
425
+ )
426
+
427
+ elif name == "semantic_search":
428
+ from .tools.semantic_search import semantic_search
429
+
430
+ result_content = await semantic_search(
431
+ query=arguments["query"],
432
+ project_path=arguments.get("project_path", "."),
433
+ n_results=arguments.get("n_results", 10),
434
+ language=arguments.get("language"),
435
+ node_type=arguments.get("node_type"),
436
+ provider=arguments.get("provider", "ollama"),
437
+ )
438
+
439
+ elif name == "hybrid_search":
440
+ from .tools.semantic_search import hybrid_search
441
+
442
+ result_content = await hybrid_search(
443
+ query=arguments["query"],
444
+ pattern=arguments.get("pattern"),
445
+ project_path=arguments.get("project_path", "."),
446
+ n_results=arguments.get("n_results", 10),
447
+ language=arguments.get("language"),
448
+ provider=arguments.get("provider", "ollama"),
449
+ )
450
+
451
+ elif name == "semantic_index":
452
+ from .tools.semantic_search import index_codebase
453
+
454
+ result_content = await index_codebase(
455
+ project_path=arguments.get("project_path", "."),
456
+ force=arguments.get("force", False),
457
+ provider=arguments.get("provider", "ollama"),
458
+ )
459
+
460
+ elif name == "semantic_stats":
461
+ from .tools.semantic_search import semantic_stats
462
+
463
+ result_content = await semantic_stats(
464
+ project_path=arguments.get("project_path", "."),
465
+ provider=arguments.get("provider", "ollama"),
466
+ )
467
+
468
+ elif name == "start_file_watcher":
469
+ from .tools.semantic_search import start_file_watcher
470
+ import json
471
+
472
+ try:
473
+ watcher = start_file_watcher(
474
+ project_path=arguments.get("project_path", "."),
475
+ provider=arguments.get("provider", "ollama"),
476
+ debounce_seconds=arguments.get("debounce_seconds", 2.0),
477
+ )
478
+
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)
486
+ except ValueError as e:
487
+ # No index exists
488
+ result_content = json.dumps({
489
+ "error": str(e),
490
+ "hint": "Run semantic_index() before starting file watcher"
491
+ }, indent=2)
492
+ print(f"⚠️ start_file_watcher ValueError: {e}", file=sys.stderr)
493
+ except Exception as e:
494
+ # Unexpected error
495
+ 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)
500
+ print(f"❌ start_file_watcher error: {e}", file=sys.stderr)
501
+ traceback.print_exc(file=sys.stderr)
502
+
503
+ elif name == "stop_file_watcher":
504
+ from .tools.semantic_search import stop_file_watcher
505
+ import json
506
+
507
+ stopped = stop_file_watcher(
508
+ project_path=arguments.get("project_path", "."),
509
+ )
510
+
511
+ result_content = json.dumps({
512
+ "stopped": stopped,
513
+ "project_path": arguments.get("project_path", ".")
514
+ }, indent=2)
515
+
516
+ elif name == "cancel_indexing":
517
+ from .tools.semantic_search import cancel_indexing
518
+
519
+ result_content = cancel_indexing(
520
+ project_path=arguments.get("project_path", "."),
521
+ provider=arguments.get("provider", "ollama"),
522
+ )
523
+
524
+ elif name == "delete_index":
525
+ from .tools.semantic_search import delete_index
526
+
527
+ result_content = delete_index(
528
+ project_path=arguments.get("project_path", "."),
529
+ provider=arguments.get("provider"), # None if not specified
530
+ delete_all=arguments.get("delete_all", False),
531
+ )
532
+
533
+ elif name == "list_file_watchers":
534
+ from .tools.semantic_search import list_file_watchers
535
+ import json
536
+
537
+ result_content = json.dumps(list_file_watchers(), indent=2)
538
+
539
+ elif name == "multi_query_search":
540
+ from .tools.semantic_search import multi_query_search
541
+
542
+ result_content = await multi_query_search(
543
+ query=arguments["query"],
544
+ project_path=arguments.get("project_path", "."),
545
+ n_results=arguments.get("n_results", 10),
546
+ num_expansions=arguments.get("num_expansions", 3),
547
+ language=arguments.get("language"),
548
+ node_type=arguments.get("node_type"),
549
+ provider=arguments.get("provider", "ollama"),
550
+ )
551
+
552
+ elif name == "decomposed_search":
553
+ from .tools.semantic_search import decomposed_search
554
+
555
+ result_content = await decomposed_search(
556
+ query=arguments["query"],
557
+ project_path=arguments.get("project_path", "."),
558
+ n_results=arguments.get("n_results", 10),
559
+ language=arguments.get("language"),
560
+ node_type=arguments.get("node_type"),
561
+ provider=arguments.get("provider", "ollama"),
562
+ )
563
+
564
+ elif name == "enhanced_search":
565
+ from .tools.semantic_search import enhanced_search
566
+
567
+ result_content = await enhanced_search(
568
+ query=arguments["query"],
569
+ project_path=arguments.get("project_path", "."),
570
+ n_results=arguments.get("n_results", 10),
571
+ mode=arguments.get("mode", "auto"),
572
+ language=arguments.get("language"),
573
+ node_type=arguments.get("node_type"),
574
+ provider=arguments.get("provider", "ollama"),
575
+ )
576
+
431
577
  else:
432
578
  result_content = f"Unknown tool: {name}"
433
579
 
@@ -441,7 +587,10 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
441
587
  processed_text = await hook_manager.execute_post_tool_call(
442
588
  name, arguments, result_content[0].text
443
589
  )
444
- result_content[0].text = processed_text
590
+ # Only update if processed_text is non-empty to avoid empty text blocks
591
+ # (API error: cache_control cannot be set for empty text blocks)
592
+ if processed_text:
593
+ result_content[0].text = processed_text
445
594
  elif isinstance(result_content, str):
446
595
  result_content = await hook_manager.execute_post_tool_call(
447
596
  name, arguments, result_content
@@ -510,6 +659,16 @@ async def async_main():
510
659
  except Exception as e:
511
660
  logger.error(f"Failed to initialize hooks: {e}")
512
661
 
662
+ # Clean up stale ChromaDB locks on startup
663
+ try:
664
+ from .tools.semantic_search import cleanup_stale_chromadb_locks
665
+
666
+ removed_count = cleanup_stale_chromadb_locks()
667
+ if removed_count > 0:
668
+ logger.info(f"Cleaned up {removed_count} stale ChromaDB lock(s)")
669
+ except Exception as e:
670
+ logger.warning(f"Failed to cleanup ChromaDB locks: {e}")
671
+
513
672
  # Start background token refresh scheduler
514
673
  try:
515
674
  from .auth.token_refresh import background_token_refresh
@@ -529,6 +688,12 @@ async def async_main():
529
688
  except Exception as e:
530
689
  logger.critical("Server process crashed in async_main", exc_info=True)
531
690
  sys.exit(1)
691
+ finally:
692
+ logger.info("Initiating shutdown sequence...")
693
+ from .tools.lsp.manager import get_lsp_manager
694
+
695
+ lsp_manager = get_lsp_manager()
696
+ await lsp_manager.shutdown()
532
697
 
533
698
 
534
699
  def main():
@@ -741,4 +906,4 @@ def main():
741
906
 
742
907
 
743
908
  if __name__ == "__main__":
744
- main()
909
+ sys.exit(main())