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
@@ -0,0 +1,75 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import socket
6
+ import time
7
+ from dataclasses import asdict, dataclass
8
+ from datetime import datetime
9
+ from typing import Any
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ SOCKET_PATH = "/tmp/stravinsky.sock"
14
+
15
+ @dataclass
16
+ class LogMessage:
17
+ agent_id: str
18
+ type: str # stdout, stderr, event, lifecycle
19
+ content: str
20
+ timestamp: str
21
+
22
+ class MuxClient:
23
+ def __init__(self, agent_id: str):
24
+ self.agent_id = agent_id
25
+ self._socket: socket.socket | None = None
26
+ self._connected = False
27
+
28
+ def connect(self):
29
+ try:
30
+ if not os.path.exists(SOCKET_PATH):
31
+ return
32
+
33
+ self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
34
+ self._socket.connect(SOCKET_PATH)
35
+ self._socket.setblocking(False)
36
+ self._connected = True
37
+ except Exception as e:
38
+ logger.debug(f"Failed to connect to mux: {e}")
39
+ self._connected = False
40
+
41
+ def log(self, content: str, stream: str = "stdout"):
42
+ if not self._connected:
43
+ self.connect()
44
+
45
+ if not self._connected or not self._socket:
46
+ return
47
+
48
+ msg = LogMessage(
49
+ agent_id=self.agent_id,
50
+ type=stream,
51
+ content=content,
52
+ timestamp=datetime.now().isoformat()
53
+ )
54
+
55
+ try:
56
+ data = json.dumps(asdict(msg)) + "\n"
57
+ self._socket.sendall(data.encode('utf-8'))
58
+ except (BrokenPipeError, OSError):
59
+ self._connected = False
60
+ self._socket.close()
61
+ self._socket = None
62
+
63
+ def close(self):
64
+ if self._socket:
65
+ self._socket.close()
66
+ self._connected = False
67
+
68
+ # Global instance for the main process
69
+ _global_mux: MuxClient | None = None
70
+
71
+ def get_mux(agent_id: str = "main") -> MuxClient:
72
+ global _global_mux
73
+ if _global_mux is None:
74
+ _global_mux = MuxClient(agent_id)
75
+ return _global_mux
@@ -10,12 +10,11 @@ import shutil
10
10
  import subprocess
11
11
  import sys
12
12
  from pathlib import Path
13
- from typing import Any, Dict, List, Optional
14
13
 
15
14
  from ..auth.token_store import TokenStore
16
15
 
17
16
 
18
- async def get_project_context(project_path: Optional[str] = None) -> str:
17
+ async def get_project_context(project_path: str | None = None) -> str:
19
18
  """
20
19
  Summarize project environment: Git status, local rules, and pending todos.
21
20
 
@@ -61,46 +61,56 @@ class QueryClassification:
61
61
  # Phase 1: Exact Pattern Detection (High Confidence)
62
62
  # Triggered when query contains quoted strings, exact identifiers with code syntax,
63
63
  # file paths, regular expressions, or known constant patterns.
64
+ # Format: (regex_pattern, indicator_name)
64
65
  PATTERN_INDICATORS = [
65
- r'["\'][\w_]+["\']', # Quoted identifiers like "authenticate()" or 'API_KEY'
66
- r'\b\w+\(\)', # Function calls with () like authenticate()
67
- r'[\w_]+\.[\w_]+', # Dot notation (Class.method) like database.query()
68
- r'[\w/]+\.\w{2,4}$', # File paths with extension
69
- r'/.*?/', # Regex patterns
70
- r'\b[A-Z_]{4,}\b', # CONSTANT_NAMES (4+ uppercase chars)
66
+ (r'\bgrep\b', 'explicit_grep'), # Explicit "grep" in query
67
+ (r'["\'][\w_()\.]+["\']', 'quoted_identifier'), # Quoted identifiers like "authenticate()" or 'API_KEY'
68
+ (r'\b\w+\(\)', 'function_call'), # Function calls with () like authenticate()
69
+ (r'[\w_]+\.[\w_]+', 'dot_notation'), # Dot notation (Class.method) like database.query()
70
+ (r'[\w/]+\.\w{2,4}$', 'file_path'), # File paths with extension
71
+ (r'/.*?/', 'regex_pattern'), # Regex patterns
72
+ (r'\b[A-Z_]{4,}\b', 'constant_name'), # CONSTANT_NAMES (4+ uppercase chars)
71
73
  ]
72
74
 
73
75
  # Phase 2: Structural Detection (High Confidence)
74
76
  # Triggered when query contains AST keywords, structural relationships,
75
77
  # or code structure terms.
78
+ # Format: (regex_pattern, indicator_name)
76
79
  STRUCTURAL_INDICATORS = [
77
- r'\b(class|function|method|async|interface)\b', # AST keywords
78
- r'\b(inherits?|extends?|implements?|overrides?)\b', # Structural relationships
79
- r'\b(decorated?)\s+(with|by)\b', # Decorator patterns
80
- r'\@\w+', # Decorator syntax
81
- r'\b(definition|declaration|signature)\b', # Code structure terms
80
+ (r'\b(class|function|method|async|interface)\b', 'ast_keyword'), # AST keywords
81
+ (r'\b(inherits?|inheriting)\b', 'inheritance'), # Inheritance
82
+ (r'\b(extends?|extending)\b', 'extends'), # Extension
83
+ (r'\b(implements?|implementing)\b', 'implements'), # Implementation
84
+ (r'\b(overrides?|overriding)\b', 'override'), # Override
85
+ (r'\b(decorated?)\s+(with|by)\b', 'decorator_pattern'), # Decorator patterns
86
+ (r'\@\w+', 'decorator_syntax'), # Decorator syntax
87
+ (r'\b(definition|declaration|signature)\b', 'code_structure'), # Code structure terms
82
88
  ]
83
89
 
84
90
  # Phase 3: Conceptual Detection (Medium-High Confidence)
85
91
  # Triggered when query contains intent verbs, how/why/where questions,
86
92
  # design patterns, conceptual nouns, or cross-cutting concerns.
93
+ # Format: (regex_pattern, indicator_name)
87
94
  SEMANTIC_INDICATORS = [
88
- r'\b(how|why|where)\s+(does|is|are)', # How/why/where questions
89
- r'\b(handles?|manages?|processes?|validates?|transforms?)\b', # Intent verbs
90
- r'\b(logic|mechanism|strategy|approach|workflow|implementation)\b', # Conceptual nouns
91
- r'\b(pattern|anti-pattern)\b', # Design patterns
92
- r'\b(authentication|authorization|caching|logging|error handling)\b', # Cross-cutting
93
- r'\bfind\s+(all\s+)?(code|places|instances|implementations)\s+that\b', # Find code pattern
95
+ (r'\bhow\s+(?:does|is|are)', 'how'), # How questions (non-capturing group)
96
+ (r'\bwhy\s+(?:does|is|are)', 'why'), # Why questions (non-capturing group)
97
+ (r'\bwhere\s+(?:does|is|are)', 'where'), # Where questions (non-capturing group)
98
+ (r'\b(handles?|manages?|processes?|validates?|validated?|transforms?)\b', 'intent'), # Intent verbs
99
+ (r'\b(logic|mechanism|strategy|approach|workflow|implementation)\b', 'conceptual'), # Conceptual nouns
100
+ (r'\b(patterns?|anti-patterns?)\b', 'design_pattern'), # Design patterns
101
+ (r'\b(authentication|authorization|caching|logging|error handling|middleware)\b', 'cross_cutting'), # Cross-cutting
102
+ (r'\bfind\s+(all\s+)?(code|places|instances|implementations)\s+that\b', 'find_pattern'), # Find code pattern
94
103
  ]
95
104
 
96
105
  # Phase 4: Hybrid Detection (Medium Confidence)
97
106
  # Triggered when query contains multiple concepts, both exact + conceptual,
98
107
  # broad scopes, or vague qualifiers.
108
+ # Format: (regex_pattern, indicator_name)
99
109
  HYBRID_INDICATORS = [
100
- r'\s+(and|then|also|plus|with)\s+', # Conjunctions
101
- r'\b(across|throughout|in all|system-wide)\b', # Broad scopes
102
- r'\b(similar|related|like|kind of|type of)\b', # Vague qualifiers
103
- r'\b(all|every|any)\s+\w+\s+(that|which|where)\b', # Broad quantifiers
110
+ (r'\s+(and|then|also|plus|with)\s+', 'conjunction'), # Conjunctions
111
+ (r'\b(across|throughout|in all|system-wide)\b', 'broad_scope'), # Broad scopes
112
+ (r'\b(similar|related|like|kind of|type of)\b', 'vague_qualifier'), # Vague qualifiers
113
+ (r'\b(all|every|any)\s+\w+\s+(that|which|where)\b', 'broad_quantifier'), # Broad quantifiers
104
114
  ]
105
115
 
106
116
  # Tool routing based on category
@@ -193,43 +203,108 @@ def classify_query(query: str) -> QueryClassification:
193
203
 
194
204
  query_lower = query_normalized.lower()
195
205
 
196
- # Phase 1: Pattern Detection
206
+ # Phase 1: Pattern Detection (use original case for case-sensitive patterns)
197
207
  pattern_matches = []
198
- for pattern in PATTERN_INDICATORS:
199
- if re.search(pattern, query_lower):
208
+ pattern_indicators = []
209
+ for pattern, indicator_name in PATTERN_INDICATORS:
210
+ # Case-insensitive for 'explicit_grep', case-sensitive for others (CONSTANTS, etc.)
211
+ query_to_match = query_lower if indicator_name == 'explicit_grep' else query_normalized
212
+ if re.search(pattern, query_to_match):
200
213
  pattern_matches.append(pattern)
214
+ pattern_indicators.append(indicator_name)
201
215
 
202
216
  # Phase 2: Structural Detection
203
217
  structural_matches = []
204
- for pattern in STRUCTURAL_INDICATORS:
218
+ structural_indicators = []
219
+ for pattern, indicator_name in STRUCTURAL_INDICATORS:
205
220
  if re.search(pattern, query_lower):
206
221
  structural_matches.append(pattern)
222
+ structural_indicators.append(indicator_name)
207
223
 
208
224
  # Phase 3: Semantic Detection
209
225
  semantic_matches = []
210
- for pattern in SEMANTIC_INDICATORS:
211
- if re.search(pattern, query_lower):
226
+ semantic_indicators = []
227
+ for pattern, indicator_name in SEMANTIC_INDICATORS:
228
+ match = re.search(pattern, query_lower)
229
+ if match:
212
230
  semantic_matches.append(pattern)
231
+ # Use captured group (matched word) if available, else use indicator name
232
+ matched_word = match.group(1) if match.groups() else indicator_name
233
+ semantic_indicators.append(matched_word if matched_word else indicator_name)
213
234
 
214
235
  # Phase 4: Hybrid Detection
215
236
  hybrid_matches = []
216
- for pattern in HYBRID_INDICATORS:
217
- if re.search(pattern, query_lower):
237
+ hybrid_indicators = []
238
+ for pattern, indicator_name in HYBRID_INDICATORS:
239
+ match = re.search(pattern, query_lower)
240
+ if match:
218
241
  hybrid_matches.append(pattern)
242
+ # Use captured group (matched word) if available, else use indicator name
243
+ matched_word = match.group(1) if match.groups() else indicator_name
244
+ hybrid_indicators.append(matched_word if matched_word else indicator_name)
219
245
 
220
246
  # Confidence Scoring
221
- # Score calculation:
222
- # - Each pattern match: +0.15
223
- # - Each structural match: +0.20
224
- # - Each semantic match: +0.15
225
- # - Each hybrid match: +0.10
247
+ # Base scores per match:
248
+ # - PATTERN: 0.50 base + 0.45 bonus for high-value patterns = 0.95 max
249
+ # - STRUCTURAL: 0.95 (single AST keyword should be high confidence)
250
+ # - SEMANTIC: 0.95 (single intent/concept should be high confidence)
251
+ # - HYBRID: 0.40 (multi-modal indicators)
252
+ # Note: Scores capped at 0.95 max
253
+
254
+ # Apply bonus for high-value patterns (CONSTANTS, quoted identifiers, explicit grep)
255
+ pattern_score = len(pattern_matches) * 0.50
256
+ if pattern_matches:
257
+ # Check if query contains CONSTANTS (4+ uppercase), quoted strings, or explicit grep
258
+ if (re.search(r'\b[A-Z_]{4,}\b', query_normalized) or
259
+ re.search(r'["\'][\w_()\.]+["\']', query_normalized) or
260
+ re.search(r'\bgrep\b', query_lower)):
261
+ pattern_score += 0.45 # Bonus to reach 0.95
262
+
226
263
  scores = {
227
- QueryCategory.PATTERN: len(pattern_matches) * 0.15,
228
- QueryCategory.STRUCTURAL: len(structural_matches) * 0.20,
229
- QueryCategory.SEMANTIC: len(semantic_matches) * 0.15,
230
- QueryCategory.HYBRID: len(hybrid_matches) * 0.10,
264
+ QueryCategory.PATTERN: pattern_score,
265
+ QueryCategory.STRUCTURAL: len(structural_matches) * 0.95,
266
+ QueryCategory.SEMANTIC: len(semantic_matches) * 0.95,
267
+ QueryCategory.HYBRID: len(hybrid_matches) * 0.40,
231
268
  }
232
269
 
270
+ # HYBRID preference logic
271
+ # Exception: Don't boost if PATTERN has high-value matches (they take precedence)
272
+ has_high_value_pattern = (
273
+ pattern_matches and
274
+ (re.search(r'\b[A-Z_]{4,}\b', query_normalized) or
275
+ re.search(r'["\'][\w_()\.]+["\']', query_normalized) or
276
+ re.search(r'\bgrep\b', query_lower))
277
+ )
278
+
279
+ # Count how many non-HYBRID categories have matches
280
+ categories_with_matches = sum([
281
+ 1 if pattern_matches else 0,
282
+ 1 if structural_matches else 0,
283
+ 1 if semantic_matches else 0,
284
+ ])
285
+
286
+ # Boost HYBRID score based on type of HYBRID indicator and what categories match
287
+ # Exception: Don't boost if PATTERN has high-value matches (they take precedence)
288
+ if hybrid_matches and not has_high_value_pattern:
289
+ # Check if we have strong HYBRID signals
290
+ # Look for the actual captured words, not indicator names
291
+ broad_scope_words = ['across', 'throughout', 'in all', 'system-wide']
292
+ conjunction_words = ['and', 'then', 'also', 'plus', 'with']
293
+ vague_words = ['related', 'like'] # Strong vague qualifiers (but not "similar" with design patterns)
294
+ has_broad_scope = any(word in str(hybrid_indicators).lower() for word in broad_scope_words)
295
+ has_conjunction = any(word in hybrid_indicators for word in conjunction_words)
296
+ has_vague = any(word in hybrid_indicators for word in vague_words)
297
+
298
+ # Boost to 0.95 if:
299
+ # 1. Multiple categories match (PATTERN+SEMANTIC, STRUCTURAL+SEMANTIC, etc.), OR
300
+ # 2. Broad scope, conjunction, or vague qualifiers (strong HYBRID signals)
301
+ if categories_with_matches >= 2 or has_broad_scope or has_conjunction or has_vague:
302
+ scores[QueryCategory.HYBRID] = 0.95
303
+ # Or if PATTERN or STRUCTURAL matches (even with just 1), boost slightly
304
+ elif pattern_matches or structural_matches:
305
+ scores[QueryCategory.HYBRID] = 0.90
306
+ # For SEMANTIC + "similar" only: don't boost above, handled by tie-breaking
307
+
233
308
  # Find maximum score
234
309
  max_score = max(scores.values())
235
310
 
@@ -253,24 +328,32 @@ def classify_query(query: str) -> QueryClassification:
253
328
  # Find all categories with maximum score (potential ties)
254
329
  winners = [cat for cat, score in scores.items() if score == max_score]
255
330
 
256
- # If tie, use HYBRID
331
+ # Tie-breaking logic
257
332
  if len(winners) > 1:
258
333
  confidence = min(max_score, 0.95)
259
- category = QueryCategory.HYBRID
334
+ # Prefer PATTERN if it has high-value matches (CONSTANTS, quoted strings, explicit grep)
335
+ if QueryCategory.PATTERN in winners and has_high_value_pattern:
336
+ category = QueryCategory.PATTERN
337
+ # Prefer SEMANTIC if it has design pattern indicators (semantic concept wins over vague "similar")
338
+ elif QueryCategory.SEMANTIC in winners and any('pattern' in str(ind).lower() for ind in semantic_indicators):
339
+ category = QueryCategory.SEMANTIC
340
+ else:
341
+ # Otherwise use HYBRID for mixed queries
342
+ category = QueryCategory.HYBRID
260
343
  else:
261
344
  confidence = min(max_score, 0.95)
262
345
  category = winners[0]
263
346
 
264
- # Gather all indicators for reporting
347
+ # Gather all indicators for reporting (use specific names)
265
348
  all_indicators = []
266
- if pattern_matches:
267
- all_indicators.append("pattern_match")
268
- if structural_matches:
269
- all_indicators.append("structural_match")
270
- if semantic_matches:
271
- all_indicators.append("semantic_match")
272
- if hybrid_matches:
273
- all_indicators.append("hybrid_match")
349
+ if pattern_indicators:
350
+ all_indicators.extend(pattern_indicators)
351
+ if structural_indicators:
352
+ all_indicators.extend(structural_indicators)
353
+ if semantic_indicators:
354
+ all_indicators.extend(semantic_indicators)
355
+ if hybrid_indicators:
356
+ all_indicators.extend(hybrid_indicators)
274
357
 
275
358
  # Generate reasoning
276
359
  reasoning_parts = []
@@ -0,0 +1,84 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Optional
4
+ from mcp_bridge.utils.truncation import truncate_output, TruncationStrategy
5
+
6
+ from mcp_bridge.utils.cache import IOCache
7
+
8
+ async def read_file(
9
+ path: str,
10
+ offset: int = 0,
11
+ limit: Optional[int] = None,
12
+ max_chars: int = 20000
13
+ ) -> str:
14
+ """
15
+ Read the contents of a file with smart truncation and log-awareness.
16
+ """
17
+ # USER-VISIBLE NOTIFICATION
18
+ import sys
19
+ print(f"📖 READ: {path} (offset={offset}, limit={limit})", file=sys.stderr)
20
+
21
+ cache = IOCache.get_instance()
22
+ cache_key = f"read_file:{os.path.realpath(path)}:{offset}:{limit}:{max_chars}"
23
+
24
+ cached_result = cache.get(cache_key)
25
+ if cached_result:
26
+ return cached_result
27
+
28
+ file_path = Path(path)
29
+ if not file_path.exists():
30
+ return f"Error: File not found: {path}"
31
+
32
+ if not file_path.is_file():
33
+ return f"Error: Path is not a file: {path}"
34
+
35
+ try:
36
+ # Detect log files
37
+ is_log = file_path.suffix.lower() in (".log", ".out", ".err")
38
+
39
+ # Read lines
40
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
41
+ lines = f.readlines()
42
+
43
+ total_lines = len(lines)
44
+
45
+ # Default behavior for log files if no limit/offset specified
46
+ if is_log and limit is None and offset == 0 and total_lines > 100:
47
+ # Default to last 100 lines for large logs
48
+ offset = max(0, total_lines - 100)
49
+ limit = 100
50
+ strategy = TruncationStrategy.TAIL
51
+ guidance = "Log file detected. Reading last 100 lines by default."
52
+ else:
53
+ strategy = TruncationStrategy.MIDDLE
54
+ guidance = None
55
+
56
+ # Apply line-based filtering
57
+ start = offset
58
+ end = total_lines
59
+ if limit is not None:
60
+ end = start + limit
61
+
62
+ selected_lines = lines[start:end]
63
+ content = "".join(selected_lines)
64
+
65
+ # Apply character-based truncation (universal cap)
66
+ result = truncate_output(
67
+ content,
68
+ limit=max_chars,
69
+ strategy=strategy,
70
+ custom_guidance=guidance
71
+ )
72
+
73
+ # If truncate_output didn't add guidance (because content < max_chars)
74
+ # but we have log-based guidance, add it manually
75
+ if guidance and guidance not in result:
76
+ result = f"{result}\n\n[{guidance}]"
77
+
78
+ # Cache for 5 seconds
79
+ cache.set(cache_key, result)
80
+
81
+ return result
82
+
83
+ except Exception as e:
84
+ return f"Error reading file {path}: {str(e)}"
@@ -0,0 +1,45 @@
1
+ import os
2
+ from pathlib import Path
3
+ from mcp_bridge.utils.cache import IOCache
4
+
5
+ async def replace(
6
+ path: str,
7
+ old_string: str,
8
+ new_string: str,
9
+ instruction: str,
10
+ expected_replacements: int = 1
11
+ ) -> str:
12
+ """
13
+ Replace text in a file and invalidate cache.
14
+ """
15
+ # USER-VISIBLE NOTIFICATION
16
+ import sys
17
+ print(f"🔄 REPLACE: {path} (instruction: {instruction})", file=sys.stderr)
18
+
19
+ file_path = Path(path)
20
+ if not file_path.exists():
21
+ return f"Error: File not found: {path}"
22
+
23
+ try:
24
+ content = file_path.read_text(encoding="utf-8")
25
+
26
+ # Check occurrence count
27
+ count = content.count(old_string)
28
+ if count == 0:
29
+ return f"Error: Could not find exact match for old_string in {path}"
30
+
31
+ if count != expected_replacements:
32
+ return f"Error: Found {count} occurrences of old_string, but expected {expected_replacements} in {path}"
33
+
34
+ # Perform replacement
35
+ new_content = content.replace(old_string, new_string)
36
+ file_path.write_text(new_content, encoding="utf-8")
37
+
38
+ # Invalidate cache
39
+ cache = IOCache.get_instance()
40
+ cache.invalidate_path(str(file_path))
41
+
42
+ return f"Successfully modified file: {path} ({count} replacements)."
43
+
44
+ except Exception as e:
45
+ return f"Error modifying file {path}: {str(e)}"
@@ -0,0 +1,38 @@
1
+ import os
2
+ from mcp_bridge.utils.cache import IOCache
3
+ from mcp_bridge.utils.process import async_execute
4
+
5
+ async def run_shell_command(command: str, description: str, dir_path: str = ".") -> str:
6
+ """
7
+ Execute a shell command and invalidate cache if it looks like a write.
8
+ """
9
+ # USER-VISIBLE NOTIFICATION
10
+ import sys
11
+ print(f"🐚 BASH: {command} ({description})", file=sys.stderr)
12
+
13
+ try:
14
+ # Run command asynchronously
15
+ result = await async_execute(command, cwd=dir_path, timeout=300)
16
+
17
+ # Check if it looks like a write command (simplistic heuristic)
18
+ write_keywords = ["git commit", "git push", "rm ", "mv ", "cp ", "touch ", "> ", ">> ", "sed ", "chmod "]
19
+ is_write = any(kw in command for kw in write_keywords)
20
+
21
+ if is_write:
22
+ # Broad invalidation for write commands
23
+ cache = IOCache.get_instance()
24
+ # If we're in a specific dir, invalidate that dir
25
+ cache.invalidate_path(os.path.abspath(dir_path))
26
+
27
+ # Format output
28
+ output = []
29
+ output.append(f"Command: {command}")
30
+ output.append(f"Directory: {dir_path}")
31
+ output.append(f"Stdout: {result.stdout}")
32
+ output.append(f"Stderr: {result.stderr}")
33
+ output.append(f"Exit Code: {result.returncode}")
34
+
35
+ return "\n".join(output)
36
+
37
+ except Exception as e:
38
+ return f"Error executing command: {str(e)}"