stravinsky 0.2.67__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 +112 -11
- 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/MANIFEST_SCHEMA.md +305 -0
- mcp_bridge/config/README.md +276 -0
- mcp_bridge/config/__init__.py +2 -2
- mcp_bridge/config/hook_config.py +247 -0
- mcp_bridge/config/hooks_manifest.json +138 -0
- mcp_bridge/config/rate_limits.py +317 -0
- mcp_bridge/config/skills_manifest.json +128 -0
- mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
- mcp_bridge/hooks/__init__.py +19 -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 +43 -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/tool_messaging.py +113 -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 +150 -0
- 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 +542 -59
- mcp_bridge/server_tools.py +738 -6
- mcp_bridge/tools/__init__.py +40 -25
- mcp_bridge/tools/agent_manager.py +616 -697
- mcp_bridge/tools/background_tasks.py +13 -17
- mcp_bridge/tools/code_search.py +70 -53
- 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 +12 -5
- mcp_bridge/tools/lsp/manager.py +471 -0
- mcp_bridge/tools/lsp/tools.py +723 -207
- mcp_bridge/tools/model_invoke.py +1195 -273
- mcp_bridge/tools/mux_client.py +75 -0
- mcp_bridge/tools/project_context.py +1 -2
- mcp_bridge/tools/query_classifier.py +406 -0
- 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 +3627 -0
- 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 +585 -0
- mcp_bridge/update_manager_pypi.py +297 -0
- 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.66.dist-info/METADATA +517 -0
- stravinsky-0.4.66.dist-info/RECORD +198 -0
- {stravinsky-0.2.67.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.2.67.dist-info/METADATA +0 -284
- stravinsky-0.2.67.dist-info/RECORD +0 -76
- {stravinsky-0.2.67.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)}"
|
|
@@ -90,6 +85,10 @@ async def check_ai_comment_patterns(file_path: str) -> str:
|
|
|
90
85
|
Returns:
|
|
91
86
|
List of detected AI-style patterns with line numbers, or "No AI patterns detected"
|
|
92
87
|
"""
|
|
88
|
+
# USER-VISIBLE NOTIFICATION
|
|
89
|
+
import sys
|
|
90
|
+
print(f"🤖 AI-CHECK: {file_path}", file=sys.stderr)
|
|
91
|
+
|
|
93
92
|
path = Path(file_path)
|
|
94
93
|
if not path.exists():
|
|
95
94
|
return f"Error: File not found: {file_path}"
|
|
@@ -158,12 +157,7 @@ async def ast_grep_search(pattern: str, directory: str = ".", language: str = ""
|
|
|
158
157
|
cmd.extend(["--lang", language])
|
|
159
158
|
cmd.append("--json")
|
|
160
159
|
|
|
161
|
-
result =
|
|
162
|
-
cmd,
|
|
163
|
-
capture_output=True,
|
|
164
|
-
text=True,
|
|
165
|
-
timeout=60,
|
|
166
|
-
)
|
|
160
|
+
result = await async_execute(cmd, timeout=60)
|
|
167
161
|
|
|
168
162
|
if result.returncode != 0 and not result.stdout:
|
|
169
163
|
return result.stderr or "No matches found"
|
|
@@ -187,15 +181,18 @@ async def ast_grep_search(pattern: str, directory: str = ".", language: str = ""
|
|
|
187
181
|
|
|
188
182
|
except FileNotFoundError:
|
|
189
183
|
return "ast-grep (sg) not found. Install with: npm install -g @ast-grep/cli"
|
|
190
|
-
except
|
|
184
|
+
except asyncio.TimeoutError:
|
|
191
185
|
return "Search timed out"
|
|
192
186
|
except Exception as e:
|
|
193
187
|
return f"Error: {str(e)}"
|
|
194
188
|
|
|
195
189
|
|
|
190
|
+
from mcp_bridge.native_search import native_glob_files, native_grep_search
|
|
191
|
+
|
|
192
|
+
|
|
196
193
|
async def grep_search(pattern: str, directory: str = ".", file_pattern: str = "") -> str:
|
|
197
194
|
"""
|
|
198
|
-
Fast text search using ripgrep.
|
|
195
|
+
Fast text search using ripgrep (or native Rust implementation if available).
|
|
199
196
|
|
|
200
197
|
Args:
|
|
201
198
|
pattern: Search pattern (supports regex)
|
|
@@ -210,17 +207,29 @@ async def grep_search(pattern: str, directory: str = ".", file_pattern: str = ""
|
|
|
210
207
|
glob_info = f" glob={file_pattern}" if file_pattern else ""
|
|
211
208
|
print(f"🔎 GREP: pattern='{pattern[:50]}'{glob_info} dir={directory}", file=sys.stderr)
|
|
212
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
|
+
|
|
213
227
|
try:
|
|
214
228
|
cmd = ["rg", "--line-number", "--max-count=50", pattern, directory]
|
|
215
229
|
if file_pattern:
|
|
216
230
|
cmd.extend(["--glob", file_pattern])
|
|
217
231
|
|
|
218
|
-
result =
|
|
219
|
-
cmd,
|
|
220
|
-
capture_output=True,
|
|
221
|
-
text=True,
|
|
222
|
-
timeout=30,
|
|
223
|
-
)
|
|
232
|
+
result = await async_execute(cmd, timeout=30)
|
|
224
233
|
|
|
225
234
|
output = result.stdout
|
|
226
235
|
if not output.strip():
|
|
@@ -230,13 +239,13 @@ async def grep_search(pattern: str, directory: str = ".", file_pattern: str = ""
|
|
|
230
239
|
lines = output.strip().split("\n")
|
|
231
240
|
if len(lines) > 50:
|
|
232
241
|
lines = lines[:50]
|
|
233
|
-
lines.append(
|
|
242
|
+
lines.append("... and more (showing first 50 matches)")
|
|
234
243
|
|
|
235
244
|
return "\n".join(lines)
|
|
236
245
|
|
|
237
246
|
except FileNotFoundError:
|
|
238
247
|
return "ripgrep (rg) not found. Install with: brew install ripgrep"
|
|
239
|
-
except
|
|
248
|
+
except asyncio.TimeoutError:
|
|
240
249
|
return "Search timed out"
|
|
241
250
|
except Exception as e:
|
|
242
251
|
return f"Error: {str(e)}"
|
|
@@ -244,24 +253,37 @@ async def grep_search(pattern: str, directory: str = ".", file_pattern: str = ""
|
|
|
244
253
|
|
|
245
254
|
async def glob_files(pattern: str, directory: str = ".") -> str:
|
|
246
255
|
"""
|
|
247
|
-
Find files matching a glob pattern.
|
|
248
|
-
|
|
256
|
+
Find files matching a glob pattern (uses native Rust implementation if available).
|
|
257
|
+
|
|
249
258
|
Args:
|
|
250
259
|
pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts")
|
|
251
260
|
directory: Base directory for search
|
|
252
|
-
|
|
261
|
+
|
|
253
262
|
Returns:
|
|
254
263
|
List of matching file paths.
|
|
255
264
|
"""
|
|
265
|
+
# USER-VISIBLE NOTIFICATION
|
|
266
|
+
import sys
|
|
267
|
+
print(f"📁 GLOB: pattern='{pattern}' dir={directory}", file=sys.stderr)
|
|
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
|
+
|
|
256
283
|
try:
|
|
257
284
|
cmd = ["fd", "--type", "f", "--glob", pattern, directory]
|
|
258
285
|
|
|
259
|
-
result =
|
|
260
|
-
cmd,
|
|
261
|
-
capture_output=True,
|
|
262
|
-
text=True,
|
|
263
|
-
timeout=30,
|
|
264
|
-
)
|
|
286
|
+
result = await async_execute(cmd, timeout=30)
|
|
265
287
|
|
|
266
288
|
output = result.stdout
|
|
267
289
|
if not output.strip():
|
|
@@ -277,7 +299,7 @@ async def glob_files(pattern: str, directory: str = ".") -> str:
|
|
|
277
299
|
|
|
278
300
|
except FileNotFoundError:
|
|
279
301
|
return "fd not found. Install with: brew install fd"
|
|
280
|
-
except
|
|
302
|
+
except asyncio.TimeoutError:
|
|
281
303
|
return "Search timed out"
|
|
282
304
|
except Exception as e:
|
|
283
305
|
return f"Error: {str(e)}"
|
|
@@ -306,6 +328,12 @@ async def ast_grep_replace(
|
|
|
306
328
|
Returns:
|
|
307
329
|
Preview of changes or confirmation of applied changes.
|
|
308
330
|
"""
|
|
331
|
+
# USER-VISIBLE NOTIFICATION
|
|
332
|
+
import sys
|
|
333
|
+
mode = "dry-run" if dry_run else "APPLY"
|
|
334
|
+
lang_info = f" lang={language}" if language else ""
|
|
335
|
+
print(f"🔄 AST-REPLACE: '{pattern[:30]}' → '{replacement[:30]}'{lang_info} [{mode}]", file=sys.stderr)
|
|
336
|
+
|
|
309
337
|
try:
|
|
310
338
|
# Build command
|
|
311
339
|
cmd = ["sg", "run", "-p", pattern, "-r", replacement, directory]
|
|
@@ -315,12 +343,7 @@ async def ast_grep_replace(
|
|
|
315
343
|
if dry_run:
|
|
316
344
|
# Show what would change
|
|
317
345
|
cmd.append("--json")
|
|
318
|
-
result =
|
|
319
|
-
cmd,
|
|
320
|
-
capture_output=True,
|
|
321
|
-
text=True,
|
|
322
|
-
timeout=60,
|
|
323
|
-
)
|
|
346
|
+
result = await async_execute(cmd, timeout=60)
|
|
324
347
|
|
|
325
348
|
if result.returncode != 0 and not result.stdout:
|
|
326
349
|
return result.stderr or "No matches found"
|
|
@@ -352,12 +375,7 @@ async def ast_grep_replace(
|
|
|
352
375
|
if language:
|
|
353
376
|
cmd_apply.extend(["--lang", language])
|
|
354
377
|
|
|
355
|
-
result =
|
|
356
|
-
cmd_apply,
|
|
357
|
-
capture_output=True,
|
|
358
|
-
text=True,
|
|
359
|
-
timeout=60,
|
|
360
|
-
)
|
|
378
|
+
result = await async_execute(cmd_apply, timeout=60)
|
|
361
379
|
|
|
362
380
|
if result.returncode == 0:
|
|
363
381
|
return f"✅ Successfully applied replacement:\n- Pattern: `{pattern}`\n- Replacement: `{replacement}`\n\n{result.stdout}"
|
|
@@ -366,8 +384,7 @@ async def ast_grep_replace(
|
|
|
366
384
|
|
|
367
385
|
except FileNotFoundError:
|
|
368
386
|
return "ast-grep (sg) not found. Install with: npm install -g @ast-grep/cli"
|
|
369
|
-
except
|
|
387
|
+
except asyncio.TimeoutError:
|
|
370
388
|
return "Replacement timed out"
|
|
371
389
|
except Exception as e:
|
|
372
|
-
return f"Error: {str(e)}"
|
|
373
|
-
|
|
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())
|