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
|
@@ -5,17 +5,13 @@ Provides mechanisms to spawn, monitor, and manage async sub-agents.
|
|
|
5
5
|
Tasks are persisted to .stravinsky/tasks.json.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import asyncio
|
|
9
8
|
import json
|
|
10
|
-
import os
|
|
11
9
|
import subprocess
|
|
12
10
|
import sys
|
|
13
|
-
import
|
|
14
|
-
import uuid
|
|
15
|
-
from dataclasses import asdict, dataclass, field
|
|
11
|
+
from dataclasses import asdict, dataclass
|
|
16
12
|
from datetime import datetime
|
|
17
13
|
from pathlib import Path
|
|
18
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
19
15
|
|
|
20
16
|
|
|
21
17
|
@dataclass
|
|
@@ -25,15 +21,15 @@ class BackgroundTask:
|
|
|
25
21
|
model: str
|
|
26
22
|
status: str # pending, running, completed, failed
|
|
27
23
|
created_at: str
|
|
28
|
-
started_at:
|
|
29
|
-
completed_at:
|
|
30
|
-
result:
|
|
31
|
-
error:
|
|
32
|
-
pid:
|
|
24
|
+
started_at: str | None = None
|
|
25
|
+
completed_at: str | None = None
|
|
26
|
+
result: str | None = None
|
|
27
|
+
error: str | None = None
|
|
28
|
+
pid: int | None = None
|
|
33
29
|
|
|
34
30
|
|
|
35
31
|
class BackgroundManager:
|
|
36
|
-
def __init__(self, base_dir:
|
|
32
|
+
def __init__(self, base_dir: str | None = None):
|
|
37
33
|
if base_dir:
|
|
38
34
|
self.base_dir = Path(base_dir)
|
|
39
35
|
else:
|
|
@@ -49,14 +45,14 @@ class BackgroundManager:
|
|
|
49
45
|
if not self.state_file.exists():
|
|
50
46
|
self._save_tasks({})
|
|
51
47
|
|
|
52
|
-
def _load_tasks(self) ->
|
|
48
|
+
def _load_tasks(self) -> dict[str, Any]:
|
|
53
49
|
try:
|
|
54
|
-
with open(self.state_file
|
|
50
|
+
with open(self.state_file) as f:
|
|
55
51
|
return json.load(f)
|
|
56
52
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
57
53
|
return {}
|
|
58
54
|
|
|
59
|
-
def _save_tasks(self, tasks:
|
|
55
|
+
def _save_tasks(self, tasks: dict[str, Any]):
|
|
60
56
|
with open(self.state_file, "w") as f:
|
|
61
57
|
json.dump(tasks, f, indent=2)
|
|
62
58
|
|
|
@@ -82,11 +78,11 @@ class BackgroundManager:
|
|
|
82
78
|
tasks[task_id].update(kwargs)
|
|
83
79
|
self._save_tasks(tasks)
|
|
84
80
|
|
|
85
|
-
def get_task(self, task_id: str) ->
|
|
81
|
+
def get_task(self, task_id: str) -> dict[str, Any] | None:
|
|
86
82
|
tasks = self._load_tasks()
|
|
87
83
|
return tasks.get(task_id)
|
|
88
84
|
|
|
89
|
-
def list_tasks(self) ->
|
|
85
|
+
def list_tasks(self) -> list[dict[str, Any]]:
|
|
90
86
|
tasks = self._load_tasks()
|
|
91
87
|
return list(tasks.values())
|
|
92
88
|
|
mcp_bridge/tools/code_search.py
CHANGED
|
@@ -6,11 +6,10 @@ to language servers. Claude Code has native LSP support, so these serve as
|
|
|
6
6
|
supplementary utilities for advanced operations.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import asyncio
|
|
10
9
|
import json
|
|
11
|
-
import
|
|
10
|
+
import asyncio
|
|
12
11
|
from pathlib import Path
|
|
13
|
-
|
|
12
|
+
from mcp_bridge.utils.process import async_execute
|
|
14
13
|
|
|
15
14
|
async def lsp_diagnostics(file_path: str, severity: str = "all") -> str:
|
|
16
15
|
"""
|
|
@@ -39,11 +38,9 @@ async def lsp_diagnostics(file_path: str, severity: str = "all") -> str:
|
|
|
39
38
|
try:
|
|
40
39
|
if suffix in (".ts", ".tsx", ".js", ".jsx"):
|
|
41
40
|
# Use TypeScript compiler for diagnostics
|
|
42
|
-
result =
|
|
41
|
+
result = await async_execute(
|
|
43
42
|
["npx", "tsc", "--noEmit", "--pretty", str(path)],
|
|
44
|
-
|
|
45
|
-
text=True,
|
|
46
|
-
timeout=30,
|
|
43
|
+
timeout=30
|
|
47
44
|
)
|
|
48
45
|
output = result.stdout + result.stderr
|
|
49
46
|
if not output.strip():
|
|
@@ -52,11 +49,9 @@ async def lsp_diagnostics(file_path: str, severity: str = "all") -> str:
|
|
|
52
49
|
|
|
53
50
|
elif suffix == ".py":
|
|
54
51
|
# Use ruff for Python diagnostics
|
|
55
|
-
result =
|
|
52
|
+
result = await async_execute(
|
|
56
53
|
["ruff", "check", str(path), "--output-format=concise"],
|
|
57
|
-
|
|
58
|
-
text=True,
|
|
59
|
-
timeout=30,
|
|
54
|
+
timeout=30
|
|
60
55
|
)
|
|
61
56
|
output = result.stdout + result.stderr
|
|
62
57
|
if not output.strip():
|
|
@@ -68,7 +63,7 @@ async def lsp_diagnostics(file_path: str, severity: str = "all") -> str:
|
|
|
68
63
|
|
|
69
64
|
except FileNotFoundError as e:
|
|
70
65
|
return f"Tool not found: {e.filename}. Install required tools."
|
|
71
|
-
except
|
|
66
|
+
except asyncio.TimeoutError:
|
|
72
67
|
return "Diagnostics timed out"
|
|
73
68
|
except Exception as e:
|
|
74
69
|
return f"Error: {str(e)}"
|
|
@@ -162,12 +157,7 @@ async def ast_grep_search(pattern: str, directory: str = ".", language: str = ""
|
|
|
162
157
|
cmd.extend(["--lang", language])
|
|
163
158
|
cmd.append("--json")
|
|
164
159
|
|
|
165
|
-
result =
|
|
166
|
-
cmd,
|
|
167
|
-
capture_output=True,
|
|
168
|
-
text=True,
|
|
169
|
-
timeout=60,
|
|
170
|
-
)
|
|
160
|
+
result = await async_execute(cmd, timeout=60)
|
|
171
161
|
|
|
172
162
|
if result.returncode != 0 and not result.stdout:
|
|
173
163
|
return result.stderr or "No matches found"
|
|
@@ -191,15 +181,18 @@ async def ast_grep_search(pattern: str, directory: str = ".", language: str = ""
|
|
|
191
181
|
|
|
192
182
|
except FileNotFoundError:
|
|
193
183
|
return "ast-grep (sg) not found. Install with: npm install -g @ast-grep/cli"
|
|
194
|
-
except
|
|
184
|
+
except asyncio.TimeoutError:
|
|
195
185
|
return "Search timed out"
|
|
196
186
|
except Exception as e:
|
|
197
187
|
return f"Error: {str(e)}"
|
|
198
188
|
|
|
199
189
|
|
|
190
|
+
from mcp_bridge.native_search import native_glob_files, native_grep_search
|
|
191
|
+
|
|
192
|
+
|
|
200
193
|
async def grep_search(pattern: str, directory: str = ".", file_pattern: str = "") -> str:
|
|
201
194
|
"""
|
|
202
|
-
Fast text search using ripgrep.
|
|
195
|
+
Fast text search using ripgrep (or native Rust implementation if available).
|
|
203
196
|
|
|
204
197
|
Args:
|
|
205
198
|
pattern: Search pattern (supports regex)
|
|
@@ -214,17 +207,29 @@ async def grep_search(pattern: str, directory: str = ".", file_pattern: str = ""
|
|
|
214
207
|
glob_info = f" glob={file_pattern}" if file_pattern else ""
|
|
215
208
|
print(f"🔎 GREP: pattern='{pattern[:50]}'{glob_info} dir={directory}", file=sys.stderr)
|
|
216
209
|
|
|
210
|
+
# Try native implementation first (currently doesn't support file_pattern filter in the same way)
|
|
211
|
+
# If file_pattern is provided, we still use rg for now as it's more flexible with globs
|
|
212
|
+
if not file_pattern:
|
|
213
|
+
native_results = await native_grep_search(pattern, directory)
|
|
214
|
+
if native_results is not None:
|
|
215
|
+
if not native_results:
|
|
216
|
+
return "No matches found"
|
|
217
|
+
|
|
218
|
+
lines = []
|
|
219
|
+
for r in native_results[:50]:
|
|
220
|
+
lines.append(f"{r['path']}:{r['line']}: {r['content']}")
|
|
221
|
+
|
|
222
|
+
if len(native_results) > 50:
|
|
223
|
+
lines.append(f"... and more (showing first 50 matches)")
|
|
224
|
+
|
|
225
|
+
return "\n".join(lines)
|
|
226
|
+
|
|
217
227
|
try:
|
|
218
228
|
cmd = ["rg", "--line-number", "--max-count=50", pattern, directory]
|
|
219
229
|
if file_pattern:
|
|
220
230
|
cmd.extend(["--glob", file_pattern])
|
|
221
231
|
|
|
222
|
-
result =
|
|
223
|
-
cmd,
|
|
224
|
-
capture_output=True,
|
|
225
|
-
text=True,
|
|
226
|
-
timeout=30,
|
|
227
|
-
)
|
|
232
|
+
result = await async_execute(cmd, timeout=30)
|
|
228
233
|
|
|
229
234
|
output = result.stdout
|
|
230
235
|
if not output.strip():
|
|
@@ -234,13 +239,13 @@ async def grep_search(pattern: str, directory: str = ".", file_pattern: str = ""
|
|
|
234
239
|
lines = output.strip().split("\n")
|
|
235
240
|
if len(lines) > 50:
|
|
236
241
|
lines = lines[:50]
|
|
237
|
-
lines.append(
|
|
242
|
+
lines.append("... and more (showing first 50 matches)")
|
|
238
243
|
|
|
239
244
|
return "\n".join(lines)
|
|
240
245
|
|
|
241
246
|
except FileNotFoundError:
|
|
242
247
|
return "ripgrep (rg) not found. Install with: brew install ripgrep"
|
|
243
|
-
except
|
|
248
|
+
except asyncio.TimeoutError:
|
|
244
249
|
return "Search timed out"
|
|
245
250
|
except Exception as e:
|
|
246
251
|
return f"Error: {str(e)}"
|
|
@@ -248,7 +253,7 @@ async def grep_search(pattern: str, directory: str = ".", file_pattern: str = ""
|
|
|
248
253
|
|
|
249
254
|
async def glob_files(pattern: str, directory: str = ".") -> str:
|
|
250
255
|
"""
|
|
251
|
-
Find files matching a glob pattern.
|
|
256
|
+
Find files matching a glob pattern (uses native Rust implementation if available).
|
|
252
257
|
|
|
253
258
|
Args:
|
|
254
259
|
pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts")
|
|
@@ -261,15 +266,24 @@ async def glob_files(pattern: str, directory: str = ".") -> str:
|
|
|
261
266
|
import sys
|
|
262
267
|
print(f"📁 GLOB: pattern='{pattern}' dir={directory}", file=sys.stderr)
|
|
263
268
|
|
|
269
|
+
# Try native implementation first
|
|
270
|
+
native_results = await native_glob_files(pattern, directory)
|
|
271
|
+
if native_results is not None:
|
|
272
|
+
if not native_results:
|
|
273
|
+
return "No files found"
|
|
274
|
+
|
|
275
|
+
# Limit output
|
|
276
|
+
lines = native_results
|
|
277
|
+
if len(lines) > 100:
|
|
278
|
+
lines = lines[:100]
|
|
279
|
+
lines.append(f"... and {len(native_results) - 100} more files")
|
|
280
|
+
|
|
281
|
+
return "\n".join(lines)
|
|
282
|
+
|
|
264
283
|
try:
|
|
265
284
|
cmd = ["fd", "--type", "f", "--glob", pattern, directory]
|
|
266
285
|
|
|
267
|
-
result =
|
|
268
|
-
cmd,
|
|
269
|
-
capture_output=True,
|
|
270
|
-
text=True,
|
|
271
|
-
timeout=30,
|
|
272
|
-
)
|
|
286
|
+
result = await async_execute(cmd, timeout=30)
|
|
273
287
|
|
|
274
288
|
output = result.stdout
|
|
275
289
|
if not output.strip():
|
|
@@ -285,7 +299,7 @@ async def glob_files(pattern: str, directory: str = ".") -> str:
|
|
|
285
299
|
|
|
286
300
|
except FileNotFoundError:
|
|
287
301
|
return "fd not found. Install with: brew install fd"
|
|
288
|
-
except
|
|
302
|
+
except asyncio.TimeoutError:
|
|
289
303
|
return "Search timed out"
|
|
290
304
|
except Exception as e:
|
|
291
305
|
return f"Error: {str(e)}"
|
|
@@ -329,12 +343,7 @@ async def ast_grep_replace(
|
|
|
329
343
|
if dry_run:
|
|
330
344
|
# Show what would change
|
|
331
345
|
cmd.append("--json")
|
|
332
|
-
result =
|
|
333
|
-
cmd,
|
|
334
|
-
capture_output=True,
|
|
335
|
-
text=True,
|
|
336
|
-
timeout=60,
|
|
337
|
-
)
|
|
346
|
+
result = await async_execute(cmd, timeout=60)
|
|
338
347
|
|
|
339
348
|
if result.returncode != 0 and not result.stdout:
|
|
340
349
|
return result.stderr or "No matches found"
|
|
@@ -366,12 +375,7 @@ async def ast_grep_replace(
|
|
|
366
375
|
if language:
|
|
367
376
|
cmd_apply.extend(["--lang", language])
|
|
368
377
|
|
|
369
|
-
result =
|
|
370
|
-
cmd_apply,
|
|
371
|
-
capture_output=True,
|
|
372
|
-
text=True,
|
|
373
|
-
timeout=60,
|
|
374
|
-
)
|
|
378
|
+
result = await async_execute(cmd_apply, timeout=60)
|
|
375
379
|
|
|
376
380
|
if result.returncode == 0:
|
|
377
381
|
return f"✅ Successfully applied replacement:\n- Pattern: `{pattern}`\n- Replacement: `{replacement}`\n\n{result.stdout}"
|
|
@@ -380,8 +384,7 @@ async def ast_grep_replace(
|
|
|
380
384
|
|
|
381
385
|
except FileNotFoundError:
|
|
382
386
|
return "ast-grep (sg) not found. Install with: npm install -g @ast-grep/cli"
|
|
383
|
-
except
|
|
387
|
+
except asyncio.TimeoutError:
|
|
384
388
|
return "Replacement timed out"
|
|
385
389
|
except Exception as e:
|
|
386
|
-
return f"Error: {str(e)}"
|
|
387
|
-
|
|
390
|
+
return f"Error: {str(e)}"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from mcp_bridge.metrics.cost_tracker import get_cost_tracker
|
|
3
|
+
|
|
4
|
+
async def get_cost_report(session_id: str | None = None) -> str:
|
|
5
|
+
"""Get a cost report for the current or specified session."""
|
|
6
|
+
tracker = get_cost_tracker()
|
|
7
|
+
summary = tracker.get_session_summary(session_id)
|
|
8
|
+
|
|
9
|
+
lines = ["## Agent Cost Report"]
|
|
10
|
+
lines.append(f"**Total Cost**: ${summary['total_cost']:.4f}")
|
|
11
|
+
lines.append(f"**Total Tokens**: {summary['total_tokens']:,}")
|
|
12
|
+
lines.append("")
|
|
13
|
+
lines.append("| Agent | Tokens | Cost |")
|
|
14
|
+
lines.append("|---|---|---|")
|
|
15
|
+
|
|
16
|
+
for agent, data in summary["by_agent"].items():
|
|
17
|
+
lines.append(f"| {agent} | {data['tokens']:,} | ${data['cost']:.4f} |")
|
|
18
|
+
|
|
19
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smart code search routing tool.
|
|
3
|
+
|
|
4
|
+
Automatically routes queries to the optimal search strategy:
|
|
5
|
+
- AST patterns (e.g., "class $X", "def $FUNC") → ast_grep_search
|
|
6
|
+
- Natural language (e.g., "authentication logic") → semantic_search
|
|
7
|
+
- Complex queries (e.g., "JWT AND middleware") → hybrid_search
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
# Import search tools
|
|
14
|
+
from mcp_bridge.tools.code_search import ast_grep_search, grep_search
|
|
15
|
+
from mcp_bridge.tools.semantic_search import semantic_search, hybrid_search
|
|
16
|
+
from mcp_bridge.tools.search_enhancements import git_context_search
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
SearchType = Literal["auto", "exact", "semantic", "hybrid", "ast", "grep", "context"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def has_ast_pattern(query: str) -> bool:
|
|
23
|
+
"""
|
|
24
|
+
Detect if query contains AST-grep pattern syntax.
|
|
25
|
+
|
|
26
|
+
AST-grep patterns use metavariables ($VAR, $$$) and structural markers.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
- "class $NAME" → True (has metavariable)
|
|
30
|
+
- "def $FUNC($$$):" → True (has metavariable and wildcard)
|
|
31
|
+
- "interface{}" → True (structural pattern)
|
|
32
|
+
- "find auth code" → False (natural language)
|
|
33
|
+
"""
|
|
34
|
+
# AST-grep metavariable patterns
|
|
35
|
+
if re.search(r'\$[A-Z_]+', query): # $VAR, $NAME, etc.
|
|
36
|
+
return True
|
|
37
|
+
if re.search(r'\$\$\$', query): # Wildcard args
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
# Common structural patterns (without natural language words)
|
|
41
|
+
structural_keywords = [
|
|
42
|
+
r'\bclass\s+\w+\s*[:{(]', # class Foo: or class Foo {
|
|
43
|
+
r'\bdef\s+\w+\s*\(', # def func(
|
|
44
|
+
r'\bfunction\s+\w+\s*\(', # function func(
|
|
45
|
+
r'\binterface\s+\w+\s*[{<]', # interface Foo {
|
|
46
|
+
r'\bstruct\s+\w+\s*[{<]', # struct Foo {
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
for pattern in structural_keywords:
|
|
50
|
+
if re.search(pattern, query):
|
|
51
|
+
# Only if it looks like code, not prose
|
|
52
|
+
# "class Foo:" is code, "class that handles auth" is prose
|
|
53
|
+
if not re.search(r'\b(that|which|handles|manages|for|with|the)\b', query, re.IGNORECASE):
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def has_boolean_operators(query: str) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Detect boolean operators indicating complex query logic.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
- "JWT AND middleware" → True
|
|
65
|
+
- "auth OR login" → True
|
|
66
|
+
- "NOT deprecated" → True
|
|
67
|
+
- "authentication logic" → False
|
|
68
|
+
"""
|
|
69
|
+
# Match boolean operators (case-insensitive, word boundaries)
|
|
70
|
+
return bool(re.search(r'\b(AND|OR|NOT)\b', query, re.IGNORECASE))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_natural_language(query: str) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
Detect if query is natural language vs code pattern.
|
|
76
|
+
|
|
77
|
+
Natural language queries use prose phrases, not code syntax.
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
- "find authentication logic" → True
|
|
81
|
+
- "error handling patterns" → True
|
|
82
|
+
- "class $NAME" → False (AST pattern)
|
|
83
|
+
- "JWT middleware" → True (conceptual)
|
|
84
|
+
"""
|
|
85
|
+
# If it's an AST pattern, it's not natural language
|
|
86
|
+
if has_ast_pattern(query):
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
# Natural language indicators
|
|
90
|
+
nl_indicators = [
|
|
91
|
+
r'\b(find|search|look for|locate|where|show|get)\b', # Action verbs
|
|
92
|
+
r'\b(all|any|every|some)\b', # Quantifiers
|
|
93
|
+
r'\b(that|which|with|using|for)\b', # Connectors
|
|
94
|
+
r'\b(logic|code|pattern|implementation|function|method|class)\b', # Meta terms
|
|
95
|
+
r'\b(how|what|when|why)\b', # Question words
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
for pattern in nl_indicators:
|
|
99
|
+
if re.search(pattern, query, re.IGNORECASE):
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
# If query has spaces and no code symbols, likely natural language
|
|
103
|
+
if ' ' in query and not re.search(r'[(){}\[\]<>;,]', query):
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def detect_search_type(query: str) -> SearchType:
|
|
110
|
+
"""
|
|
111
|
+
Auto-detect optimal search type based on query pattern.
|
|
112
|
+
|
|
113
|
+
Detection logic:
|
|
114
|
+
1. AST pattern → "ast" (ast_grep_search)
|
|
115
|
+
2. Boolean operators + natural language → "hybrid" (hybrid_search)
|
|
116
|
+
3. Natural language → "semantic" (semantic_search)
|
|
117
|
+
4. Simple text → "grep" (grep_search)
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
query: Search query string
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Detected search type (ast/hybrid/semantic/grep)
|
|
124
|
+
"""
|
|
125
|
+
# Priority 1: AST patterns
|
|
126
|
+
if has_ast_pattern(query):
|
|
127
|
+
return "ast"
|
|
128
|
+
|
|
129
|
+
# Priority 2: Complex boolean queries
|
|
130
|
+
if has_boolean_operators(query):
|
|
131
|
+
return "hybrid"
|
|
132
|
+
|
|
133
|
+
# Priority 3: Natural language
|
|
134
|
+
if is_natural_language(query):
|
|
135
|
+
return "semantic"
|
|
136
|
+
|
|
137
|
+
# Default: Simple text search
|
|
138
|
+
return "grep"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def find_code(
|
|
142
|
+
query: str,
|
|
143
|
+
search_type: SearchType = "auto",
|
|
144
|
+
project_path: str = ".",
|
|
145
|
+
language: str | None = None,
|
|
146
|
+
n_results: int = 10,
|
|
147
|
+
provider: str = "ollama",
|
|
148
|
+
) -> str:
|
|
149
|
+
"""
|
|
150
|
+
Smart code search with automatic routing to optimal search strategy.
|
|
151
|
+
|
|
152
|
+
Automatically detects whether query is:
|
|
153
|
+
- AST pattern (e.g., "class $X") → routes to ast_grep_search
|
|
154
|
+
- Natural language (e.g., "auth logic") → routes to semantic_search
|
|
155
|
+
- Complex query (e.g., "JWT AND middleware") → routes to hybrid_search
|
|
156
|
+
- Simple text → routes to grep_search
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
query: Search query (pattern or natural language)
|
|
160
|
+
search_type: Search strategy ("auto" for detection, or "ast"/"semantic"/"hybrid"/"grep")
|
|
161
|
+
project_path: Path to project root (default: ".")
|
|
162
|
+
language: Filter by language (e.g., "py", "ts", "js")
|
|
163
|
+
n_results: Maximum results to return (default: 10)
|
|
164
|
+
provider: Embedding provider for semantic search (default: "ollama")
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Formatted search results with file paths and code snippets.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
# AST pattern search (auto-detected)
|
|
171
|
+
find_code("class $NAME")
|
|
172
|
+
|
|
173
|
+
# Semantic search (auto-detected)
|
|
174
|
+
find_code("authentication logic")
|
|
175
|
+
|
|
176
|
+
# Hybrid search (auto-detected)
|
|
177
|
+
find_code("JWT AND middleware")
|
|
178
|
+
|
|
179
|
+
# Force specific search type
|
|
180
|
+
find_code("error handling", search_type="semantic")
|
|
181
|
+
"""
|
|
182
|
+
# Auto-detect search type if requested
|
|
183
|
+
if search_type == "auto":
|
|
184
|
+
detected_type = detect_search_type(query)
|
|
185
|
+
search_type = detected_type
|
|
186
|
+
|
|
187
|
+
# Route to appropriate search tool
|
|
188
|
+
if search_type == "ast":
|
|
189
|
+
# AST-grep search for structural patterns
|
|
190
|
+
return await ast_grep_search(
|
|
191
|
+
pattern=query,
|
|
192
|
+
directory=project_path,
|
|
193
|
+
language=language or "",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
elif search_type == "semantic":
|
|
197
|
+
# Semantic search for natural language queries
|
|
198
|
+
return await semantic_search(
|
|
199
|
+
query=query,
|
|
200
|
+
project_path=project_path,
|
|
201
|
+
n_results=n_results,
|
|
202
|
+
language=language,
|
|
203
|
+
provider=provider, # type: ignore
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
elif search_type == "hybrid":
|
|
207
|
+
# Hybrid search for complex queries
|
|
208
|
+
# Parse boolean operators into pattern if possible
|
|
209
|
+
pattern = None
|
|
210
|
+
if has_boolean_operators(query):
|
|
211
|
+
# For now, pass full query to semantic, rely on hybrid's logic
|
|
212
|
+
# Future: parse "JWT AND middleware" into pattern
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
return await hybrid_search(
|
|
216
|
+
query=query,
|
|
217
|
+
pattern=pattern,
|
|
218
|
+
project_path=project_path,
|
|
219
|
+
n_results=n_results,
|
|
220
|
+
language=language,
|
|
221
|
+
provider=provider, # type: ignore
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
elif search_type in ("grep", "exact"):
|
|
225
|
+
# Text-based grep search
|
|
226
|
+
file_pattern = ""
|
|
227
|
+
if language:
|
|
228
|
+
# Map language to file extension
|
|
229
|
+
lang_map = {
|
|
230
|
+
"py": "*.py",
|
|
231
|
+
"python": "*.py",
|
|
232
|
+
"ts": "*.ts",
|
|
233
|
+
"typescript": "*.ts",
|
|
234
|
+
"js": "*.js",
|
|
235
|
+
"javascript": "*.js",
|
|
236
|
+
"tsx": "*.tsx",
|
|
237
|
+
"jsx": "*.jsx",
|
|
238
|
+
"go": "*.go",
|
|
239
|
+
"rust": "*.rs",
|
|
240
|
+
"java": "*.java",
|
|
241
|
+
"cpp": "*.cpp",
|
|
242
|
+
"c": "*.c",
|
|
243
|
+
}
|
|
244
|
+
file_pattern = lang_map.get(language.lower(), f"*.{language}")
|
|
245
|
+
|
|
246
|
+
return await grep_search(
|
|
247
|
+
pattern=query,
|
|
248
|
+
directory=project_path,
|
|
249
|
+
file_pattern=file_pattern,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
elif search_type == "context":
|
|
253
|
+
# Git context search
|
|
254
|
+
return await git_context_search(
|
|
255
|
+
target_file=query,
|
|
256
|
+
project_path=project_path,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
else:
|
|
260
|
+
return f"Error: Unknown search_type '{search_type}'. Use 'auto', 'ast', 'semantic', 'hybrid', 'grep', or 'context'."
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Example usage and testing
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
import asyncio
|
|
266
|
+
|
|
267
|
+
# Test pattern detection
|
|
268
|
+
test_cases = [
|
|
269
|
+
("class $NAME", "ast"),
|
|
270
|
+
("def $FUNC($$$):", "ast"),
|
|
271
|
+
("find authentication logic", "semantic"),
|
|
272
|
+
("error handling patterns", "semantic"),
|
|
273
|
+
("JWT AND middleware", "hybrid"),
|
|
274
|
+
("auth OR login", "hybrid"),
|
|
275
|
+
("import os", "grep"),
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
print("Pattern Detection Tests:")
|
|
279
|
+
print("=" * 60)
|
|
280
|
+
for query, expected in test_cases:
|
|
281
|
+
detected = detect_search_type(query)
|
|
282
|
+
status = "✅" if detected == expected else "❌"
|
|
283
|
+
print(f"{status} '{query}' → {detected} (expected: {expected})")
|
|
284
|
+
|
|
285
|
+
# Test actual search (requires running codebase)
|
|
286
|
+
async def test_search():
|
|
287
|
+
print("\n\nSearch Tests:")
|
|
288
|
+
print("=" * 60)
|
|
289
|
+
|
|
290
|
+
# Test semantic search
|
|
291
|
+
result = await find_code("authentication logic", search_type="auto")
|
|
292
|
+
print(f"\nQuery: 'authentication logic'")
|
|
293
|
+
print(f"Result: {result[:200]}...")
|
|
294
|
+
|
|
295
|
+
# Uncomment to run tests
|
|
296
|
+
# asyncio.run(test_search())
|
mcp_bridge/tools/init.py
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from mcp_bridge.utils.cache import IOCache
|
|
4
|
+
|
|
5
|
+
async def list_directory(path: str) -> str:
|
|
6
|
+
"""
|
|
7
|
+
List files and directories in a path with caching.
|
|
8
|
+
"""
|
|
9
|
+
# USER-VISIBLE NOTIFICATION
|
|
10
|
+
import sys
|
|
11
|
+
print(f"📂 LIST: {path}", file=sys.stderr)
|
|
12
|
+
|
|
13
|
+
cache = IOCache.get_instance()
|
|
14
|
+
cache_key = f"list_dir:{os.path.realpath(path)}"
|
|
15
|
+
|
|
16
|
+
cached_result = cache.get(cache_key)
|
|
17
|
+
if cached_result:
|
|
18
|
+
return cached_result
|
|
19
|
+
|
|
20
|
+
dir_path = Path(path)
|
|
21
|
+
if not dir_path.exists():
|
|
22
|
+
return f"Error: Directory not found: {path}"
|
|
23
|
+
|
|
24
|
+
if not dir_path.is_dir():
|
|
25
|
+
return f"Error: Path is not a directory: {path}"
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
entries = []
|
|
29
|
+
# Sort for deterministic output
|
|
30
|
+
for entry in sorted(dir_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
|
|
31
|
+
entry_type = "DIR" if entry.is_dir() else "FILE"
|
|
32
|
+
entries.append(f"[{entry_type}] {entry.name}")
|
|
33
|
+
|
|
34
|
+
result = "\n".join(entries) if entries else "(empty directory)"
|
|
35
|
+
|
|
36
|
+
# Cache for 5 seconds
|
|
37
|
+
cache.set(cache_key, result)
|
|
38
|
+
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return f"Error listing directory {path}: {str(e)}"
|