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
@@ -6,10 +6,8 @@ Sessions are stored in ~/.claude/projects/ as JSONL files.
6
6
  """
7
7
 
8
8
  import json
9
- import os
10
9
  from datetime import datetime
11
10
  from pathlib import Path
12
- from typing import Any
13
11
 
14
12
 
15
13
  def get_sessions_directory() -> Path:
@@ -8,7 +8,6 @@ Discovers and lists available skills (slash commands) from:
8
8
  Skills are markdown files with frontmatter defining the command behavior.
9
9
  """
10
10
 
11
- import os
12
11
  import re
13
12
  from pathlib import Path
14
13
  from typing import Any
@@ -6,12 +6,10 @@ capture output, and update status in tasks.json.
6
6
  """
7
7
 
8
8
  import argparse
9
+ import asyncio
9
10
  import json
10
11
  import logging
11
12
  import os
12
- import sys
13
- import asyncio
14
- import subprocess
15
13
  from datetime import datetime
16
14
  from pathlib import Path
17
15
 
@@ -27,7 +25,7 @@ async def run_task(task_id: str, base_dir: str):
27
25
 
28
26
  # Load task details
29
27
  try:
30
- with open(tasks_file, "r") as f:
28
+ with open(tasks_file) as f:
31
29
  tasks = json.load(f)
32
30
  task = tasks.get(task_id)
33
31
  except Exception as e:
@@ -39,7 +37,7 @@ async def run_task(task_id: str, base_dir: str):
39
37
  return
40
38
 
41
39
  prompt = task.get("prompt")
42
- model = task.get("model", "gemini-2.0-flash")
40
+ model = task.get("model", "gemini-3-flash")
43
41
 
44
42
  output_file = agents_dir / f"{task_id}.out"
45
43
  agents_dir.mkdir(parents=True, exist_ok=True)
@@ -83,7 +81,7 @@ async def run_task(task_id: str, base_dir: str):
83
81
  f.write(result)
84
82
 
85
83
  # Update status
86
- with open(tasks_file, "r") as f:
84
+ with open(tasks_file) as f:
87
85
  tasks = json.load(f)
88
86
 
89
87
  if task_id in tasks:
@@ -104,7 +102,7 @@ async def run_task(task_id: str, base_dir: str):
104
102
 
105
103
  # Update status with error
106
104
  try:
107
- with open(tasks_file, "r") as f:
105
+ with open(tasks_file) as f:
108
106
  tasks = json.load(f)
109
107
  if task_id in tasks:
110
108
  tasks[task_id].update(
@@ -33,7 +33,7 @@ For ANY task with 2+ independent steps:
33
33
  3. Monitor with agent_progress, collect with agent_output
34
34
 
35
35
  ### Trigger Commands
36
- - **IRONSTAR** / **IRS**: Maximum parallel execution - spawn agents aggressively for every subtask
36
+ - **ULTRAWORK** / **UW**: Maximum parallel execution - spawn agents aggressively for every subtask
37
37
  - **ULTRATHINK**: Engage exhaustive deep reasoning, multi-dimensional analysis
38
38
  - **SEARCH**: Maximize search effort across codebase and external resources
39
39
  - **ANALYZE**: Deep analysis mode with delphi consultation for complex issues
@@ -99,11 +99,11 @@ stravinsky:agent_output(task_id="[id]", block=true)
99
99
  - This enables auto-delegation without manual /stravinsky invocation
100
100
 
101
101
  ### Execution Modes:
102
- - `ironstar` / `irs` / `ultrawork` - Maximum parallel execution (10+ agents)
102
+ - `ultrawork` / `irs` - Maximum parallel execution (10+ agents)
103
103
  - `ultrathink` - Deep reasoning with delphi consultation
104
104
  - `search` - Exhaustive multi-agent search
105
105
 
106
- **Your FIRST action must be spawning agents, not using Read/Search tools.**
106
+ **Your FUWT action must be spawning agents, not using Read/Search tools.**
107
107
  """
108
108
 
109
109
  COMMAND_PARALLEL = """---
@@ -0,0 +1,331 @@
1
+ """
2
+ Tool Search - BM25-based relevance search across MCP tools
3
+
4
+ Provides intelligent tool discovery using BM25 (Okapi) ranking algorithm.
5
+ Enables queries like "find github tools", "search semantic code", etc.
6
+
7
+ Features:
8
+ - BM25Okapi relevance scoring across tool names, descriptions, parameters
9
+ - Tag filtering (e.g., "github", "lsp", "semantic")
10
+ - Category filtering (e.g., "search", "code", "git")
11
+ - Top-K result ranking with scores
12
+ - Comprehensive error handling with timeouts
13
+ - Query logging for debugging
14
+
15
+ Architecture:
16
+ - Uses rank-bm25 for fast text-based relevance ranking
17
+ - Searches across: tool name, description, parameter names, parameter descriptions, tags
18
+ - Returns ranked list of tool names with relevance scores
19
+ - Supports both broad queries ("find code") and specific queries ("github file search")
20
+ """
21
+
22
+ import logging
23
+ import signal
24
+ from contextlib import contextmanager
25
+ from typing import Any
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Lazy import to avoid startup cost
30
+ _bm25 = None
31
+ _import_lock = None
32
+
33
+
34
+ def get_bm25():
35
+ """Lazy import of rank_bm25."""
36
+ global _bm25, _import_lock
37
+ if _bm25 is None:
38
+ if _import_lock is None:
39
+ import threading
40
+
41
+ _import_lock = threading.Lock()
42
+
43
+ with _import_lock:
44
+ if _bm25 is None:
45
+ try:
46
+ from rank_bm25 import BM25Okapi
47
+
48
+ _bm25 = BM25Okapi
49
+ except ImportError as e:
50
+ raise ImportError(
51
+ "rank-bm25 is required for tool search. "
52
+ "Install with: uv add rank-bm25"
53
+ ) from e
54
+ return _bm25
55
+
56
+
57
+ class TimeoutError(Exception):
58
+ """Raised when tool search times out."""
59
+
60
+ pass
61
+
62
+
63
+ @contextmanager
64
+ def timeout(seconds: int):
65
+ """Context manager for operation timeout."""
66
+
67
+ def timeout_handler(signum, frame):
68
+ raise TimeoutError(f"Tool search timed out after {seconds} seconds")
69
+
70
+ # Set the signal handler
71
+ original_handler = signal.signal(signal.SIGALRM, timeout_handler)
72
+ signal.alarm(seconds)
73
+
74
+ try:
75
+ yield
76
+ finally:
77
+ # Restore original handler and cancel alarm
78
+ signal.alarm(0)
79
+ signal.signal(signal.SIGALRM, original_handler)
80
+
81
+
82
+ def extract_tool_text(tool: Any) -> str:
83
+ """
84
+ Extract searchable text from a tool definition.
85
+
86
+ Args:
87
+ tool: Tool object (Pydantic model from mcp.types) with name, description, inputSchema, etc.
88
+
89
+ Returns:
90
+ Combined text for BM25 indexing (lowercased, space-separated).
91
+ """
92
+ parts = []
93
+
94
+ # Tool name (most important - add multiple times for weight boost)
95
+ name = getattr(tool, "name", "")
96
+ if name:
97
+ parts.extend([name, name, name]) # Triple weight for name
98
+
99
+ # Description (second most important - add twice)
100
+ description = getattr(tool, "description", "")
101
+ if description:
102
+ parts.extend([description, description])
103
+
104
+ # Parameter names and descriptions
105
+ input_schema = getattr(tool, "inputSchema", {})
106
+ properties = input_schema.get("properties", {}) if isinstance(input_schema, dict) else {}
107
+ for param_name, param_info in properties.items():
108
+ parts.append(param_name)
109
+ param_desc = param_info.get("description", "") if isinstance(param_info, dict) else ""
110
+ if param_desc:
111
+ parts.append(param_desc)
112
+
113
+ # Tags (if present - add twice for importance)
114
+ # Note: tags may be in tool.meta if present
115
+ meta = getattr(tool, "meta", None)
116
+ tags = meta.get("tags", []) if meta and isinstance(meta, dict) else []
117
+ if tags:
118
+ parts.extend(tags)
119
+ parts.extend(tags) # Double weight for tags
120
+
121
+ # Category (if present - add twice)
122
+ category = meta.get("category", "") if meta and isinstance(meta, dict) else ""
123
+ if category:
124
+ parts.extend([category, category])
125
+
126
+ # Join and tokenize
127
+ text = " ".join(str(p) for p in parts if p).lower()
128
+ return text
129
+
130
+
131
+ def tokenize(text: str) -> list[str]:
132
+ """
133
+ Simple tokenization for BM25.
134
+
135
+ Args:
136
+ text: Input text to tokenize.
137
+
138
+ Returns:
139
+ List of lowercase tokens (split on whitespace and punctuation).
140
+ """
141
+ # Replace common punctuation with spaces
142
+ for char in ".,;:!?()[]{}\"'`-_/\\|@#$%^&*+=<>":
143
+ text = text.replace(char, " ")
144
+
145
+ # Split on whitespace and filter empty strings
146
+ tokens = [t.lower() for t in text.split() if t.strip()]
147
+ return tokens
148
+
149
+
150
+ def search_tools(
151
+ query: str,
152
+ tools: list[Any],
153
+ top_k: int = 10,
154
+ tag_filter: str | None = None,
155
+ category_filter: str | None = None,
156
+ timeout_seconds: int = 5,
157
+ ) -> list[dict[str, Any]]:
158
+ """
159
+ Search tools using BM25 relevance ranking.
160
+
161
+ Args:
162
+ query: Natural language search query (e.g., "find github tools")
163
+ tools: List of Tool objects (Pydantic models from mcp.types)
164
+ top_k: Maximum number of results to return (default: 10)
165
+ tag_filter: Optional tag to filter by (e.g., "github", "lsp")
166
+ category_filter: Optional category to filter by (e.g., "search", "code")
167
+ timeout_seconds: Maximum execution time (default: 5 seconds)
168
+
169
+ Returns:
170
+ List of dicts with keys: name, score, tool (original tool object)
171
+ Sorted by relevance score (highest first).
172
+
173
+ Raises:
174
+ TimeoutError: If search exceeds timeout_seconds
175
+ ValueError: If query is empty or tools list is invalid
176
+ ImportError: If rank-bm25 is not installed
177
+
178
+ Example:
179
+ >>> tools = [...] # List of Tool objects from MCP
180
+ >>> results = search_tools("find github tools", tools, top_k=5)
181
+ >>> print(results[0]["name"]) # "github_search"
182
+ """
183
+ # Input validation
184
+ if not query or not query.strip():
185
+ raise ValueError("Query cannot be empty")
186
+
187
+ if not tools or not isinstance(tools, list):
188
+ raise ValueError("Tools must be a non-empty list")
189
+
190
+ if top_k <= 0:
191
+ raise ValueError("top_k must be positive")
192
+
193
+ logger.info(
194
+ f"Tool search: query='{query}', tools={len(tools)}, "
195
+ f"tag_filter={tag_filter}, category_filter={category_filter}, top_k={top_k}"
196
+ )
197
+
198
+ try:
199
+ with timeout(timeout_seconds):
200
+ # Apply filters first (reduce search space)
201
+ filtered_tools = tools
202
+ if tag_filter:
203
+ tag_lower = tag_filter.lower()
204
+ filtered_tools = [
205
+ t
206
+ for t in filtered_tools
207
+ if tag_lower in [
208
+ tag.lower()
209
+ for tag in (getattr(t, "meta", {}).get("tags", []) if isinstance(getattr(t, "meta", None), dict) else [])
210
+ ]
211
+ ]
212
+ logger.debug(f"Tag filter '{tag_filter}' reduced to {len(filtered_tools)} tools")
213
+
214
+ if category_filter:
215
+ cat_lower = category_filter.lower()
216
+ filtered_tools = [
217
+ t for t in filtered_tools
218
+ if (getattr(t, "meta", {}).get("category", "") if isinstance(getattr(t, "meta", None), dict) else "").lower() == cat_lower
219
+ ]
220
+ logger.debug(
221
+ f"Category filter '{category_filter}' reduced to {len(filtered_tools)} tools"
222
+ )
223
+
224
+ if not filtered_tools:
225
+ logger.warning("No tools remaining after filtering")
226
+ return []
227
+
228
+ # Extract and tokenize tool text
229
+ tool_texts = [extract_tool_text(t) for t in filtered_tools]
230
+ tokenized_corpus = [tokenize(text) for text in tool_texts]
231
+
232
+ # Initialize BM25
233
+ BM25Okapi = get_bm25()
234
+ bm25 = BM25Okapi(tokenized_corpus)
235
+
236
+ # Tokenize query
237
+ query_tokens = tokenize(query)
238
+ logger.debug(f"Query tokens: {query_tokens}")
239
+
240
+ # Score all documents
241
+ scores = bm25.get_scores(query_tokens)
242
+
243
+ # Create results with scores
244
+ results = [
245
+ {"name": getattr(tool, "name", ""), "score": float(score), "tool": tool}
246
+ for tool, score in zip(filtered_tools, scores)
247
+ ]
248
+
249
+ # Sort by score (descending) and take top K
250
+ results.sort(key=lambda x: x["score"], reverse=True)
251
+ top_results = results[:top_k]
252
+
253
+ # Log results
254
+ logger.info(
255
+ f"Tool search results: {len(top_results)} tools found "
256
+ f"(top score: {top_results[0]['score']:.2f} if top_results else 0)"
257
+ )
258
+ for i, result in enumerate(top_results[:5], 1): # Log top 5
259
+ logger.debug(f" {i}. {result['name']} (score: {result['score']:.2f})")
260
+
261
+ return top_results
262
+
263
+ except TimeoutError:
264
+ logger.error(f"Tool search timed out after {timeout_seconds}s: query='{query}'")
265
+ raise
266
+ except Exception as e:
267
+ logger.error(f"Tool search failed: query='{query}', error={e}", exc_info=True)
268
+ raise
269
+
270
+
271
+ def search_tool_names(
272
+ query: str,
273
+ tools: list[Any],
274
+ top_k: int = 10,
275
+ tag_filter: str | None = None,
276
+ category_filter: str | None = None,
277
+ ) -> list[str]:
278
+ """
279
+ Convenience function that returns just tool names (no scores).
280
+
281
+ Args:
282
+ query: Search query
283
+ tools: List of Tool objects (Pydantic models from mcp.types)
284
+ top_k: Maximum results
285
+ tag_filter: Optional tag filter
286
+ category_filter: Optional tag filter
287
+
288
+ Returns:
289
+ List of tool names ordered by relevance.
290
+
291
+ Example:
292
+ >>> tools = [...] # List of Tool objects
293
+ >>> names = search_tool_names("github search", tools, top_k=5)
294
+ >>> print(names) # ["github_search", "search_code", ...]
295
+ """
296
+ results = search_tools(query, tools, top_k, tag_filter, category_filter)
297
+ return [r["name"] for r in results]
298
+
299
+
300
+ def format_search_results(results: list[dict[str, Any]], include_scores: bool = True) -> str:
301
+ """
302
+ Format search results for display.
303
+
304
+ Args:
305
+ results: List of result dicts from search_tools()
306
+ include_scores: Whether to include relevance scores
307
+
308
+ Returns:
309
+ Formatted string for display.
310
+
311
+ Example:
312
+ >>> results = search_tools("github", tools)
313
+ >>> print(format_search_results(results))
314
+ Found 3 tools:
315
+ 1. github_search (score: 8.52)
316
+ 2. github_create_issue (score: 6.31)
317
+ 3. search_code (score: 2.14)
318
+ """
319
+ if not results:
320
+ return "No tools found"
321
+
322
+ lines = [f"Found {len(results)} tool(s):"]
323
+ for i, result in enumerate(results, 1):
324
+ name = result["name"]
325
+ if include_scores:
326
+ score = result["score"]
327
+ lines.append(f"{i}. {name} (score: {score:.2f})")
328
+ else:
329
+ lines.append(f"{i}. {name}")
330
+
331
+ return "\n".join(lines)
@@ -0,0 +1,29 @@
1
+ import os
2
+ from pathlib import Path
3
+ from mcp_bridge.utils.cache import IOCache
4
+
5
+ async def write_file(path: str, content: str) -> str:
6
+ """
7
+ Write content to a file and invalidate cache.
8
+ """
9
+ # USER-VISIBLE NOTIFICATION
10
+ import sys
11
+ print(f"📝 WRITE: {path} ({len(content)} bytes)", file=sys.stderr)
12
+
13
+ file_path = Path(path)
14
+ try:
15
+ # Ensure parent directories exist
16
+ file_path.parent.mkdir(parents=True, exist_ok=True)
17
+
18
+ # Write file
19
+ file_path.write_text(content, encoding="utf-8")
20
+
21
+ # Invalidate cache for this path and its parent (directory listing)
22
+ cache = IOCache.get_instance()
23
+ cache.invalidate_path(str(file_path))
24
+ cache.invalidate_path(str(file_path.parent))
25
+
26
+ return f"Successfully wrote {len(content)} bytes to {path}"
27
+
28
+ except Exception as e:
29
+ return f"Error writing file {path}: {str(e)}"
@@ -17,19 +17,19 @@ import json
17
17
  import logging
18
18
  import shutil
19
19
  import sys
20
+ from dataclasses import asdict, dataclass
20
21
  from datetime import datetime
21
22
  from pathlib import Path
22
- from typing import Dict, List, Optional, Tuple, Any
23
- from dataclasses import dataclass, asdict
23
+ from typing import Any
24
24
 
25
25
 
26
26
  @dataclass
27
27
  class MergeConflict:
28
28
  """Represents a merge conflict for a file."""
29
29
  file_path: str
30
- base_version: Optional[str]
31
- user_version: Optional[str]
32
- new_version: Optional[str]
30
+ base_version: str | None
31
+ user_version: str | None
32
+ new_version: str | None
33
33
  conflict_type: str
34
34
 
35
35
 
@@ -38,13 +38,13 @@ class UpdateManifest:
38
38
  """Manifest tracking file versions and update status."""
39
39
  version: str
40
40
  timestamp: str
41
- files: Dict[str, str]
41
+ files: dict[str, str]
42
42
 
43
- def to_dict(self) -> Dict[str, Any]:
43
+ def to_dict(self) -> dict[str, Any]:
44
44
  return asdict(self)
45
45
 
46
46
  @staticmethod
47
- def from_dict(data: Dict[str, Any]) -> 'UpdateManifest':
47
+ def from_dict(data: dict[str, Any]) -> 'UpdateManifest':
48
48
  return UpdateManifest(
49
49
  version=data.get('version', ''),
50
50
  timestamp=data.get('timestamp', ''),
@@ -102,7 +102,7 @@ class UpdateManager:
102
102
  except Exception:
103
103
  return "unknown"
104
104
 
105
- def _load_manifest(self, manifest_type: str) -> Optional[UpdateManifest]:
105
+ def _load_manifest(self, manifest_type: str) -> UpdateManifest | None:
106
106
  """Load manifest file (base, user, new)."""
107
107
  manifest_path = self.manifest_dir / f"{manifest_type}_manifest.json"
108
108
 
@@ -132,7 +132,7 @@ class UpdateManager:
132
132
  self.logger.error(f"Failed to save manifest: {e}")
133
133
  return False
134
134
 
135
- def _create_backup(self, source_dir: Path, backup_name: str) -> Optional[Path]:
135
+ def _create_backup(self, source_dir: Path, backup_name: str) -> Path | None:
136
136
  """Create timestamped backup of directory."""
137
137
  if self.dry_run:
138
138
  return None
@@ -151,7 +151,7 @@ class UpdateManager:
151
151
  self.logger.error(f"Failed to create backup: {e}")
152
152
  return None
153
153
 
154
- def _read_file_safely(self, path: Path) -> Optional[str]:
154
+ def _read_file_safely(self, path: Path) -> str | None:
155
155
  """Read file with error handling."""
156
156
  try:
157
157
  if not path.exists():
@@ -179,11 +179,11 @@ class UpdateManager:
179
179
 
180
180
  def _detect_conflicts(
181
181
  self,
182
- base: Optional[str],
183
- user: Optional[str],
184
- new: Optional[str],
182
+ base: str | None,
183
+ user: str | None,
184
+ new: str | None,
185
185
  file_path: str
186
- ) -> Optional[MergeConflict]:
186
+ ) -> MergeConflict | None:
187
187
  """Detect merge conflicts using 3-way merge logic."""
188
188
  if new == base:
189
189
  return None
@@ -210,18 +210,16 @@ class UpdateManager:
210
210
 
211
211
  def _merge_3way(
212
212
  self,
213
- base: Optional[str],
214
- user: Optional[str],
215
- new: Optional[str],
213
+ base: str | None,
214
+ user: str | None,
215
+ new: str | None,
216
216
  file_path: str
217
- ) -> Tuple[str, bool]:
217
+ ) -> tuple[str, bool]:
218
218
  """Perform 3-way merge on file content."""
219
219
  if base is None:
220
220
  if user is None:
221
221
  return new or "", False
222
- elif new is None:
223
- return user, False
224
- elif user == new:
222
+ elif new is None or user == new:
225
223
  return user, False
226
224
  else:
227
225
  return self._format_conflict_markers(user, new), True
@@ -247,7 +245,7 @@ class UpdateManager:
247
245
 
248
246
  return user, False
249
247
 
250
- def _line_based_merge(self, base: str, user: str, new: str) -> Tuple[str, bool]:
248
+ def _line_based_merge(self, base: str, user: str, new: str) -> tuple[str, bool]:
251
249
  """Perform line-based merge for text conflicts."""
252
250
  base_lines = base.splitlines(keepends=True)
253
251
  user_lines = user.splitlines(keepends=True)
@@ -262,9 +260,7 @@ class UpdateManager:
262
260
  merged.append(u)
263
261
  elif u == b and n != b:
264
262
  merged.append(n)
265
- elif n == b and u != b:
266
- merged.append(u)
267
- elif u == n:
263
+ elif n == b and u != b or u == n:
268
264
  merged.append(u)
269
265
  else:
270
266
  merged.append(f"<<<<<<< {u}======= {n}>>>>>>> ")
@@ -279,7 +275,7 @@ class UpdateManager:
279
275
 
280
276
  return "".join(merged), has_conflict
281
277
 
282
- def _format_conflict_markers(self, user: Optional[str], new: Optional[str]) -> str:
278
+ def _format_conflict_markers(self, user: str | None, new: str | None) -> str:
283
279
  """Format conflict markers for display."""
284
280
  lines = ["<<<<<<< USER VERSION\n"]
285
281
  if user:
@@ -294,7 +290,7 @@ class UpdateManager:
294
290
  lines.append(">>>>>>> NEW VERSION\n")
295
291
  return "".join(lines)
296
292
 
297
- def _preserve_statusline(self, settings_file: Path) -> Optional[Dict[str, Any]]:
293
+ def _preserve_statusline(self, settings_file: Path) -> dict[str, Any] | None:
298
294
  """Read and preserve statusline from settings.json."""
299
295
  try:
300
296
  if not settings_file.exists():
@@ -310,10 +306,10 @@ class UpdateManager:
310
306
 
311
307
  def _merge_settings_json(
312
308
  self,
313
- base: Optional[Dict[str, Any]],
314
- user: Optional[Dict[str, Any]],
315
- new: Optional[Dict[str, Any]]
316
- ) -> Tuple[Dict[str, Any], List[MergeConflict]]:
309
+ base: dict[str, Any] | None,
310
+ user: dict[str, Any] | None,
311
+ new: dict[str, Any] | None
312
+ ) -> tuple[dict[str, Any], list[MergeConflict]]:
317
313
  """Merge settings.json with special handling for hooks and statusline."""
318
314
  conflicts = []
319
315
 
@@ -369,9 +365,9 @@ class UpdateManager:
369
365
 
370
366
  def update_hooks(
371
367
  self,
372
- new_hooks: Dict[str, str],
368
+ new_hooks: dict[str, str],
373
369
  stravinsky_version: str
374
- ) -> Tuple[bool, List[MergeConflict]]:
370
+ ) -> tuple[bool, list[MergeConflict]]:
375
371
  """Update hooks with 3-way merge and conflict detection."""
376
372
  self.logger.info(f"Starting hooks update to version {stravinsky_version}")
377
373
 
@@ -430,7 +426,7 @@ class UpdateManager:
430
426
  self.logger.info(f"Hooks update completed ({len(updated_files)} files updated)")
431
427
  return True, conflicts
432
428
 
433
- def update_settings_json(self, new_settings: Dict[str, Any]) -> Tuple[bool, List[MergeConflict]]:
429
+ def update_settings_json(self, new_settings: dict[str, Any]) -> tuple[bool, list[MergeConflict]]:
434
430
  """Update settings.json with hook merging and statusline preservation."""
435
431
  self.logger.info("Starting settings.json update")
436
432
 
@@ -495,7 +491,7 @@ class UpdateManager:
495
491
 
496
492
  return success
497
493
 
498
- def verify_integrity(self) -> Tuple[bool, List[str]]:
494
+ def verify_integrity(self) -> tuple[bool, list[str]]:
499
495
  """Verify integrity of installed hooks and settings."""
500
496
  issues = []
501
497
  hooks_dir = self.global_claude_dir / "hooks"
@@ -524,7 +520,7 @@ class UpdateManager:
524
520
 
525
521
  return len(issues) == 0, issues
526
522
 
527
- def list_backups(self) -> List[Dict[str, Any]]:
523
+ def list_backups(self) -> list[dict[str, Any]]:
528
524
  """List all available backups."""
529
525
  backups = []
530
526