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.

Files changed (190) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/__init__.py +16 -6
  3. mcp_bridge/auth/cli.py +202 -11
  4. mcp_bridge/auth/oauth.py +1 -2
  5. mcp_bridge/auth/openai_oauth.py +4 -7
  6. mcp_bridge/auth/token_store.py +112 -11
  7. mcp_bridge/cli/__init__.py +1 -1
  8. mcp_bridge/cli/install_hooks.py +503 -107
  9. mcp_bridge/cli/session_report.py +0 -3
  10. mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
  11. mcp_bridge/config/README.md +276 -0
  12. mcp_bridge/config/__init__.py +2 -2
  13. mcp_bridge/config/hook_config.py +247 -0
  14. mcp_bridge/config/hooks_manifest.json +138 -0
  15. mcp_bridge/config/rate_limits.py +317 -0
  16. mcp_bridge/config/skills_manifest.json +128 -0
  17. mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
  18. mcp_bridge/hooks/__init__.py +19 -4
  19. mcp_bridge/hooks/agent_reminder.py +4 -4
  20. mcp_bridge/hooks/auto_slash_command.py +5 -5
  21. mcp_bridge/hooks/budget_optimizer.py +2 -2
  22. mcp_bridge/hooks/claude_limits_hook.py +114 -0
  23. mcp_bridge/hooks/comment_checker.py +3 -4
  24. mcp_bridge/hooks/compaction.py +2 -2
  25. mcp_bridge/hooks/context.py +2 -1
  26. mcp_bridge/hooks/context_monitor.py +2 -2
  27. mcp_bridge/hooks/delegation_policy.py +85 -0
  28. mcp_bridge/hooks/directory_context.py +3 -3
  29. mcp_bridge/hooks/edit_recovery.py +3 -2
  30. mcp_bridge/hooks/edit_recovery_policy.py +49 -0
  31. mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
  32. mcp_bridge/hooks/events.py +160 -0
  33. mcp_bridge/hooks/git_noninteractive.py +4 -4
  34. mcp_bridge/hooks/keyword_detector.py +8 -10
  35. mcp_bridge/hooks/manager.py +43 -22
  36. mcp_bridge/hooks/notification_hook.py +13 -6
  37. mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
  38. mcp_bridge/hooks/parallel_enforcer.py +5 -5
  39. mcp_bridge/hooks/parallel_execution.py +22 -10
  40. mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
  41. mcp_bridge/hooks/pre_compact.py +8 -9
  42. mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
  43. mcp_bridge/hooks/preemptive_compaction.py +2 -3
  44. mcp_bridge/hooks/routing_notifications.py +80 -0
  45. mcp_bridge/hooks/rules_injector.py +11 -19
  46. mcp_bridge/hooks/session_idle.py +4 -4
  47. mcp_bridge/hooks/session_notifier.py +4 -4
  48. mcp_bridge/hooks/session_recovery.py +4 -5
  49. mcp_bridge/hooks/stravinsky_mode.py +1 -1
  50. mcp_bridge/hooks/subagent_stop.py +1 -3
  51. mcp_bridge/hooks/task_validator.py +2 -2
  52. mcp_bridge/hooks/tmux_manager.py +7 -8
  53. mcp_bridge/hooks/todo_delegation.py +4 -1
  54. mcp_bridge/hooks/todo_enforcer.py +180 -10
  55. mcp_bridge/hooks/tool_messaging.py +113 -10
  56. mcp_bridge/hooks/truncation_policy.py +37 -0
  57. mcp_bridge/hooks/truncator.py +1 -2
  58. mcp_bridge/metrics/cost_tracker.py +115 -0
  59. mcp_bridge/native_search.py +93 -0
  60. mcp_bridge/native_watcher.py +118 -0
  61. mcp_bridge/notifications.py +150 -0
  62. mcp_bridge/orchestrator/enums.py +11 -0
  63. mcp_bridge/orchestrator/router.py +165 -0
  64. mcp_bridge/orchestrator/state.py +32 -0
  65. mcp_bridge/orchestrator/visualization.py +14 -0
  66. mcp_bridge/orchestrator/wisdom.py +34 -0
  67. mcp_bridge/prompts/__init__.py +1 -8
  68. mcp_bridge/prompts/dewey.py +1 -1
  69. mcp_bridge/prompts/planner.py +2 -4
  70. mcp_bridge/prompts/stravinsky.py +53 -31
  71. mcp_bridge/proxy/__init__.py +0 -0
  72. mcp_bridge/proxy/client.py +70 -0
  73. mcp_bridge/proxy/model_server.py +157 -0
  74. mcp_bridge/routing/__init__.py +43 -0
  75. mcp_bridge/routing/config.py +250 -0
  76. mcp_bridge/routing/model_tiers.py +135 -0
  77. mcp_bridge/routing/provider_state.py +261 -0
  78. mcp_bridge/routing/task_classifier.py +190 -0
  79. mcp_bridge/server.py +542 -59
  80. mcp_bridge/server_tools.py +738 -6
  81. mcp_bridge/tools/__init__.py +40 -25
  82. mcp_bridge/tools/agent_manager.py +616 -697
  83. mcp_bridge/tools/background_tasks.py +13 -17
  84. mcp_bridge/tools/code_search.py +70 -53
  85. mcp_bridge/tools/continuous_loop.py +0 -1
  86. mcp_bridge/tools/dashboard.py +19 -0
  87. mcp_bridge/tools/find_code.py +296 -0
  88. mcp_bridge/tools/init.py +1 -0
  89. mcp_bridge/tools/list_directory.py +42 -0
  90. mcp_bridge/tools/lsp/__init__.py +12 -5
  91. mcp_bridge/tools/lsp/manager.py +471 -0
  92. mcp_bridge/tools/lsp/tools.py +723 -207
  93. mcp_bridge/tools/model_invoke.py +1195 -273
  94. mcp_bridge/tools/mux_client.py +75 -0
  95. mcp_bridge/tools/project_context.py +1 -2
  96. mcp_bridge/tools/query_classifier.py +406 -0
  97. mcp_bridge/tools/read_file.py +84 -0
  98. mcp_bridge/tools/replace.py +45 -0
  99. mcp_bridge/tools/run_shell_command.py +38 -0
  100. mcp_bridge/tools/search_enhancements.py +347 -0
  101. mcp_bridge/tools/semantic_search.py +3627 -0
  102. mcp_bridge/tools/session_manager.py +0 -2
  103. mcp_bridge/tools/skill_loader.py +0 -1
  104. mcp_bridge/tools/task_runner.py +5 -7
  105. mcp_bridge/tools/templates.py +3 -3
  106. mcp_bridge/tools/tool_search.py +331 -0
  107. mcp_bridge/tools/write_file.py +29 -0
  108. mcp_bridge/update_manager.py +585 -0
  109. mcp_bridge/update_manager_pypi.py +297 -0
  110. mcp_bridge/utils/cache.py +82 -0
  111. mcp_bridge/utils/process.py +71 -0
  112. mcp_bridge/utils/session_state.py +51 -0
  113. mcp_bridge/utils/truncation.py +76 -0
  114. stravinsky-0.4.66.dist-info/METADATA +517 -0
  115. stravinsky-0.4.66.dist-info/RECORD +198 -0
  116. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
  117. stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
  118. stravinsky_claude_assets/agents/HOOKS.md +437 -0
  119. stravinsky_claude_assets/agents/code-reviewer.md +210 -0
  120. stravinsky_claude_assets/agents/comment_checker.md +580 -0
  121. stravinsky_claude_assets/agents/debugger.md +254 -0
  122. stravinsky_claude_assets/agents/delphi.md +495 -0
  123. stravinsky_claude_assets/agents/dewey.md +248 -0
  124. stravinsky_claude_assets/agents/explore.md +1198 -0
  125. stravinsky_claude_assets/agents/frontend.md +472 -0
  126. stravinsky_claude_assets/agents/implementation-lead.md +164 -0
  127. stravinsky_claude_assets/agents/momus.md +464 -0
  128. stravinsky_claude_assets/agents/research-lead.md +141 -0
  129. stravinsky_claude_assets/agents/stravinsky.md +730 -0
  130. stravinsky_claude_assets/commands/delphi.md +9 -0
  131. stravinsky_claude_assets/commands/dewey.md +54 -0
  132. stravinsky_claude_assets/commands/git-master.md +112 -0
  133. stravinsky_claude_assets/commands/index.md +49 -0
  134. stravinsky_claude_assets/commands/publish.md +86 -0
  135. stravinsky_claude_assets/commands/review.md +73 -0
  136. stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
  137. stravinsky_claude_assets/commands/str/agent_list.md +56 -0
  138. stravinsky_claude_assets/commands/str/agent_output.md +92 -0
  139. stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
  140. stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
  141. stravinsky_claude_assets/commands/str/cancel.md +51 -0
  142. stravinsky_claude_assets/commands/str/clean.md +97 -0
  143. stravinsky_claude_assets/commands/str/continue.md +38 -0
  144. stravinsky_claude_assets/commands/str/index.md +199 -0
  145. stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
  146. stravinsky_claude_assets/commands/str/search.md +205 -0
  147. stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
  148. stravinsky_claude_assets/commands/str/stats.md +71 -0
  149. stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
  150. stravinsky_claude_assets/commands/str/unwatch.md +42 -0
  151. stravinsky_claude_assets/commands/str/watch.md +45 -0
  152. stravinsky_claude_assets/commands/strav.md +53 -0
  153. stravinsky_claude_assets/commands/stravinsky.md +292 -0
  154. stravinsky_claude_assets/commands/verify.md +60 -0
  155. stravinsky_claude_assets/commands/version.md +5 -0
  156. stravinsky_claude_assets/hooks/README.md +248 -0
  157. stravinsky_claude_assets/hooks/comment_checker.py +193 -0
  158. stravinsky_claude_assets/hooks/context.py +38 -0
  159. stravinsky_claude_assets/hooks/context_monitor.py +153 -0
  160. stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
  161. stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
  162. stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
  163. stravinsky_claude_assets/hooks/notification_hook.py +103 -0
  164. stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
  165. stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
  166. stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
  167. stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
  168. stravinsky_claude_assets/hooks/pre_compact.py +123 -0
  169. stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
  170. stravinsky_claude_assets/hooks/session_recovery.py +263 -0
  171. stravinsky_claude_assets/hooks/stop_hook.py +89 -0
  172. stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
  173. stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
  174. stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
  175. stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
  176. stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
  177. stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
  178. stravinsky_claude_assets/hooks/truncator.py +23 -0
  179. stravinsky_claude_assets/rules/deployment_safety.md +51 -0
  180. stravinsky_claude_assets/rules/integration_wiring.md +89 -0
  181. stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
  182. stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
  183. stravinsky_claude_assets/settings.json +152 -0
  184. stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
  185. stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
  186. stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
  187. stravinsky_claude_assets/task_dependencies.json +34 -0
  188. stravinsky-0.2.67.dist-info/METADATA +0 -284
  189. stravinsky-0.2.67.dist-info/RECORD +0 -76
  190. {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 time
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, Dict, List, Optional
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: Optional[str] = None
29
- completed_at: Optional[str] = None
30
- result: Optional[str] = None
31
- error: Optional[str] = None
32
- pid: Optional[int] = None
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: Optional[str] = None):
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) -> Dict[str, Any]:
48
+ def _load_tasks(self) -> dict[str, Any]:
53
49
  try:
54
- with open(self.state_file, "r") as f:
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: Dict[str, Any]):
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) -> Optional[Dict[str, Any]]:
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) -> List[Dict[str, Any]]:
85
+ def list_tasks(self) -> list[dict[str, Any]]:
90
86
  tasks = self._load_tasks()
91
87
  return list(tasks.values())
92
88
 
@@ -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 subprocess
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 = subprocess.run(
41
+ result = await async_execute(
43
42
  ["npx", "tsc", "--noEmit", "--pretty", str(path)],
44
- capture_output=True,
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 = subprocess.run(
52
+ result = await async_execute(
56
53
  ["ruff", "check", str(path), "--output-format=concise"],
57
- capture_output=True,
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 subprocess.TimeoutExpired:
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 = subprocess.run(
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 subprocess.TimeoutExpired:
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 = subprocess.run(
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(f"... and more (showing first 50 matches)")
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 subprocess.TimeoutExpired:
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 = subprocess.run(
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 subprocess.TimeoutExpired:
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 = subprocess.run(
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 = subprocess.run(
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 subprocess.TimeoutExpired:
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)}"
@@ -7,7 +7,6 @@ Allows Stravinsky to operate in an autonomous loop until criteria are met.
7
7
  import json
8
8
  import logging
9
9
  from pathlib import Path
10
- from typing import Any, Dict
11
10
 
12
11
  logger = logging.getLogger(__name__)
13
12
 
@@ -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
@@ -4,6 +4,7 @@ Repository bootstrap logic for Stravinsky.
4
4
 
5
5
  import logging
6
6
  from pathlib import Path
7
+
7
8
  from .templates import CLAUDE_MD_TEMPLATE, SLASH_COMMANDS
8
9
 
9
10
  logger = logging.getLogger(__name__)