stravinsky 0.4.18__py3-none-any.whl → 0.4.66__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of stravinsky might be problematic. Click here for more details.

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