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.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/auth/__init__.py +16 -6
- mcp_bridge/auth/cli.py +202 -11
- mcp_bridge/auth/oauth.py +1 -2
- mcp_bridge/auth/openai_oauth.py +4 -7
- mcp_bridge/auth/token_store.py +0 -1
- mcp_bridge/cli/__init__.py +1 -1
- mcp_bridge/cli/install_hooks.py +503 -107
- mcp_bridge/cli/session_report.py +0 -3
- mcp_bridge/config/__init__.py +2 -2
- mcp_bridge/config/hook_config.py +3 -5
- mcp_bridge/config/rate_limits.py +108 -13
- mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
- mcp_bridge/hooks/__init__.py +14 -4
- mcp_bridge/hooks/agent_reminder.py +4 -4
- mcp_bridge/hooks/auto_slash_command.py +5 -5
- mcp_bridge/hooks/budget_optimizer.py +2 -2
- mcp_bridge/hooks/claude_limits_hook.py +114 -0
- mcp_bridge/hooks/comment_checker.py +3 -4
- mcp_bridge/hooks/compaction.py +2 -2
- mcp_bridge/hooks/context.py +2 -1
- mcp_bridge/hooks/context_monitor.py +2 -2
- mcp_bridge/hooks/delegation_policy.py +85 -0
- mcp_bridge/hooks/directory_context.py +3 -3
- mcp_bridge/hooks/edit_recovery.py +3 -2
- mcp_bridge/hooks/edit_recovery_policy.py +49 -0
- mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
- mcp_bridge/hooks/events.py +160 -0
- mcp_bridge/hooks/git_noninteractive.py +4 -4
- mcp_bridge/hooks/keyword_detector.py +8 -10
- mcp_bridge/hooks/manager.py +35 -22
- mcp_bridge/hooks/notification_hook.py +13 -6
- mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
- mcp_bridge/hooks/parallel_enforcer.py +5 -5
- mcp_bridge/hooks/parallel_execution.py +22 -10
- mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
- mcp_bridge/hooks/pre_compact.py +8 -9
- mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
- mcp_bridge/hooks/preemptive_compaction.py +2 -3
- mcp_bridge/hooks/routing_notifications.py +80 -0
- mcp_bridge/hooks/rules_injector.py +11 -19
- mcp_bridge/hooks/session_idle.py +4 -4
- mcp_bridge/hooks/session_notifier.py +4 -4
- mcp_bridge/hooks/session_recovery.py +4 -5
- mcp_bridge/hooks/stravinsky_mode.py +1 -1
- mcp_bridge/hooks/subagent_stop.py +1 -3
- mcp_bridge/hooks/task_validator.py +2 -2
- mcp_bridge/hooks/tmux_manager.py +7 -8
- mcp_bridge/hooks/todo_delegation.py +4 -1
- mcp_bridge/hooks/todo_enforcer.py +180 -10
- mcp_bridge/hooks/truncation_policy.py +37 -0
- mcp_bridge/hooks/truncator.py +1 -2
- mcp_bridge/metrics/cost_tracker.py +115 -0
- mcp_bridge/native_search.py +93 -0
- mcp_bridge/native_watcher.py +118 -0
- mcp_bridge/notifications.py +3 -4
- mcp_bridge/orchestrator/enums.py +11 -0
- mcp_bridge/orchestrator/router.py +165 -0
- mcp_bridge/orchestrator/state.py +32 -0
- mcp_bridge/orchestrator/visualization.py +14 -0
- mcp_bridge/orchestrator/wisdom.py +34 -0
- mcp_bridge/prompts/__init__.py +1 -8
- mcp_bridge/prompts/dewey.py +1 -1
- mcp_bridge/prompts/planner.py +2 -4
- mcp_bridge/prompts/stravinsky.py +53 -31
- mcp_bridge/proxy/__init__.py +0 -0
- mcp_bridge/proxy/client.py +70 -0
- mcp_bridge/proxy/model_server.py +157 -0
- mcp_bridge/routing/__init__.py +43 -0
- mcp_bridge/routing/config.py +250 -0
- mcp_bridge/routing/model_tiers.py +135 -0
- mcp_bridge/routing/provider_state.py +261 -0
- mcp_bridge/routing/task_classifier.py +190 -0
- mcp_bridge/server.py +363 -34
- mcp_bridge/server_tools.py +298 -6
- mcp_bridge/tools/__init__.py +19 -8
- mcp_bridge/tools/agent_manager.py +549 -799
- mcp_bridge/tools/background_tasks.py +13 -17
- mcp_bridge/tools/code_search.py +54 -51
- mcp_bridge/tools/continuous_loop.py +0 -1
- mcp_bridge/tools/dashboard.py +19 -0
- mcp_bridge/tools/find_code.py +296 -0
- mcp_bridge/tools/init.py +1 -0
- mcp_bridge/tools/list_directory.py +42 -0
- mcp_bridge/tools/lsp/__init__.py +8 -8
- mcp_bridge/tools/lsp/manager.py +51 -28
- mcp_bridge/tools/lsp/tools.py +98 -65
- mcp_bridge/tools/model_invoke.py +1047 -152
- mcp_bridge/tools/mux_client.py +75 -0
- mcp_bridge/tools/project_context.py +1 -2
- mcp_bridge/tools/query_classifier.py +132 -49
- mcp_bridge/tools/read_file.py +84 -0
- mcp_bridge/tools/replace.py +45 -0
- mcp_bridge/tools/run_shell_command.py +38 -0
- mcp_bridge/tools/search_enhancements.py +347 -0
- mcp_bridge/tools/semantic_search.py +677 -92
- mcp_bridge/tools/session_manager.py +0 -2
- mcp_bridge/tools/skill_loader.py +0 -1
- mcp_bridge/tools/task_runner.py +5 -7
- mcp_bridge/tools/templates.py +3 -3
- mcp_bridge/tools/tool_search.py +331 -0
- mcp_bridge/tools/write_file.py +29 -0
- mcp_bridge/update_manager.py +33 -37
- mcp_bridge/update_manager_pypi.py +6 -8
- mcp_bridge/utils/cache.py +82 -0
- mcp_bridge/utils/process.py +71 -0
- mcp_bridge/utils/session_state.py +51 -0
- mcp_bridge/utils/truncation.py +76 -0
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/METADATA +84 -35
- stravinsky-0.4.66.dist-info/RECORD +198 -0
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
- stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
- stravinsky_claude_assets/agents/HOOKS.md +437 -0
- stravinsky_claude_assets/agents/code-reviewer.md +210 -0
- stravinsky_claude_assets/agents/comment_checker.md +580 -0
- stravinsky_claude_assets/agents/debugger.md +254 -0
- stravinsky_claude_assets/agents/delphi.md +495 -0
- stravinsky_claude_assets/agents/dewey.md +248 -0
- stravinsky_claude_assets/agents/explore.md +1198 -0
- stravinsky_claude_assets/agents/frontend.md +472 -0
- stravinsky_claude_assets/agents/implementation-lead.md +164 -0
- stravinsky_claude_assets/agents/momus.md +464 -0
- stravinsky_claude_assets/agents/research-lead.md +141 -0
- stravinsky_claude_assets/agents/stravinsky.md +730 -0
- stravinsky_claude_assets/commands/delphi.md +9 -0
- stravinsky_claude_assets/commands/dewey.md +54 -0
- stravinsky_claude_assets/commands/git-master.md +112 -0
- stravinsky_claude_assets/commands/index.md +49 -0
- stravinsky_claude_assets/commands/publish.md +86 -0
- stravinsky_claude_assets/commands/review.md +73 -0
- stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
- stravinsky_claude_assets/commands/str/agent_list.md +56 -0
- stravinsky_claude_assets/commands/str/agent_output.md +92 -0
- stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
- stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
- stravinsky_claude_assets/commands/str/cancel.md +51 -0
- stravinsky_claude_assets/commands/str/clean.md +97 -0
- stravinsky_claude_assets/commands/str/continue.md +38 -0
- stravinsky_claude_assets/commands/str/index.md +199 -0
- stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
- stravinsky_claude_assets/commands/str/search.md +205 -0
- stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
- stravinsky_claude_assets/commands/str/stats.md +71 -0
- stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
- stravinsky_claude_assets/commands/str/unwatch.md +42 -0
- stravinsky_claude_assets/commands/str/watch.md +45 -0
- stravinsky_claude_assets/commands/strav.md +53 -0
- stravinsky_claude_assets/commands/stravinsky.md +292 -0
- stravinsky_claude_assets/commands/verify.md +60 -0
- stravinsky_claude_assets/commands/version.md +5 -0
- stravinsky_claude_assets/hooks/README.md +248 -0
- stravinsky_claude_assets/hooks/comment_checker.py +193 -0
- stravinsky_claude_assets/hooks/context.py +38 -0
- stravinsky_claude_assets/hooks/context_monitor.py +153 -0
- stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
- stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
- stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
- stravinsky_claude_assets/hooks/notification_hook.py +103 -0
- stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
- stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
- stravinsky_claude_assets/hooks/pre_compact.py +123 -0
- stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
- stravinsky_claude_assets/hooks/session_recovery.py +263 -0
- stravinsky_claude_assets/hooks/stop_hook.py +89 -0
- stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
- stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
- stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
- stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
- stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
- stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
- stravinsky_claude_assets/hooks/truncator.py +23 -0
- stravinsky_claude_assets/rules/deployment_safety.md +51 -0
- stravinsky_claude_assets/rules/integration_wiring.md +89 -0
- stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
- stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
- stravinsky_claude_assets/settings.json +152 -0
- stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
- stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
- stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
- stravinsky_claude_assets/task_dependencies.json +34 -0
- stravinsky-0.4.18.dist-info/RECORD +0 -88
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
mcp_bridge/tools/skill_loader.py
CHANGED
mcp_bridge/tools/task_runner.py
CHANGED
|
@@ -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
|
|
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-
|
|
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
|
|
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
|
|
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(
|
mcp_bridge/tools/templates.py
CHANGED
|
@@ -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
|
-
- **
|
|
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
|
-
- `
|
|
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
|
|
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)}"
|
mcp_bridge/update_manager.py
CHANGED
|
@@ -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
|
|
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:
|
|
31
|
-
user_version:
|
|
32
|
-
new_version:
|
|
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:
|
|
41
|
+
files: dict[str, str]
|
|
42
42
|
|
|
43
|
-
def to_dict(self) ->
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
44
|
return asdict(self)
|
|
45
45
|
|
|
46
46
|
@staticmethod
|
|
47
|
-
def from_dict(data:
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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:
|
|
183
|
-
user:
|
|
184
|
-
new:
|
|
182
|
+
base: str | None,
|
|
183
|
+
user: str | None,
|
|
184
|
+
new: str | None,
|
|
185
185
|
file_path: str
|
|
186
|
-
) ->
|
|
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:
|
|
214
|
-
user:
|
|
215
|
-
new:
|
|
213
|
+
base: str | None,
|
|
214
|
+
user: str | None,
|
|
215
|
+
new: str | None,
|
|
216
216
|
file_path: str
|
|
217
|
-
) ->
|
|
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) ->
|
|
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:
|
|
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) ->
|
|
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:
|
|
314
|
-
user:
|
|
315
|
-
new:
|
|
316
|
-
) ->
|
|
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:
|
|
368
|
+
new_hooks: dict[str, str],
|
|
373
369
|
stravinsky_version: str
|
|
374
|
-
) ->
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
523
|
+
def list_backups(self) -> list[dict[str, Any]]:
|
|
528
524
|
"""List all available backups."""
|
|
529
525
|
backups = []
|
|
530
526
|
|