stravinsky 0.4.18__py3-none-any.whl → 0.4.66__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of stravinsky might be problematic. Click here for more details.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/auth/__init__.py +16 -6
- mcp_bridge/auth/cli.py +202 -11
- mcp_bridge/auth/oauth.py +1 -2
- mcp_bridge/auth/openai_oauth.py +4 -7
- mcp_bridge/auth/token_store.py +0 -1
- mcp_bridge/cli/__init__.py +1 -1
- mcp_bridge/cli/install_hooks.py +503 -107
- mcp_bridge/cli/session_report.py +0 -3
- mcp_bridge/config/__init__.py +2 -2
- mcp_bridge/config/hook_config.py +3 -5
- mcp_bridge/config/rate_limits.py +108 -13
- mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
- mcp_bridge/hooks/__init__.py +14 -4
- mcp_bridge/hooks/agent_reminder.py +4 -4
- mcp_bridge/hooks/auto_slash_command.py +5 -5
- mcp_bridge/hooks/budget_optimizer.py +2 -2
- mcp_bridge/hooks/claude_limits_hook.py +114 -0
- mcp_bridge/hooks/comment_checker.py +3 -4
- mcp_bridge/hooks/compaction.py +2 -2
- mcp_bridge/hooks/context.py +2 -1
- mcp_bridge/hooks/context_monitor.py +2 -2
- mcp_bridge/hooks/delegation_policy.py +85 -0
- mcp_bridge/hooks/directory_context.py +3 -3
- mcp_bridge/hooks/edit_recovery.py +3 -2
- mcp_bridge/hooks/edit_recovery_policy.py +49 -0
- mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
- mcp_bridge/hooks/events.py +160 -0
- mcp_bridge/hooks/git_noninteractive.py +4 -4
- mcp_bridge/hooks/keyword_detector.py +8 -10
- mcp_bridge/hooks/manager.py +35 -22
- mcp_bridge/hooks/notification_hook.py +13 -6
- mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
- mcp_bridge/hooks/parallel_enforcer.py +5 -5
- mcp_bridge/hooks/parallel_execution.py +22 -10
- mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
- mcp_bridge/hooks/pre_compact.py +8 -9
- mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
- mcp_bridge/hooks/preemptive_compaction.py +2 -3
- mcp_bridge/hooks/routing_notifications.py +80 -0
- mcp_bridge/hooks/rules_injector.py +11 -19
- mcp_bridge/hooks/session_idle.py +4 -4
- mcp_bridge/hooks/session_notifier.py +4 -4
- mcp_bridge/hooks/session_recovery.py +4 -5
- mcp_bridge/hooks/stravinsky_mode.py +1 -1
- mcp_bridge/hooks/subagent_stop.py +1 -3
- mcp_bridge/hooks/task_validator.py +2 -2
- mcp_bridge/hooks/tmux_manager.py +7 -8
- mcp_bridge/hooks/todo_delegation.py +4 -1
- mcp_bridge/hooks/todo_enforcer.py +180 -10
- mcp_bridge/hooks/truncation_policy.py +37 -0
- mcp_bridge/hooks/truncator.py +1 -2
- mcp_bridge/metrics/cost_tracker.py +115 -0
- mcp_bridge/native_search.py +93 -0
- mcp_bridge/native_watcher.py +118 -0
- mcp_bridge/notifications.py +3 -4
- mcp_bridge/orchestrator/enums.py +11 -0
- mcp_bridge/orchestrator/router.py +165 -0
- mcp_bridge/orchestrator/state.py +32 -0
- mcp_bridge/orchestrator/visualization.py +14 -0
- mcp_bridge/orchestrator/wisdom.py +34 -0
- mcp_bridge/prompts/__init__.py +1 -8
- mcp_bridge/prompts/dewey.py +1 -1
- mcp_bridge/prompts/planner.py +2 -4
- mcp_bridge/prompts/stravinsky.py +53 -31
- mcp_bridge/proxy/__init__.py +0 -0
- mcp_bridge/proxy/client.py +70 -0
- mcp_bridge/proxy/model_server.py +157 -0
- mcp_bridge/routing/__init__.py +43 -0
- mcp_bridge/routing/config.py +250 -0
- mcp_bridge/routing/model_tiers.py +135 -0
- mcp_bridge/routing/provider_state.py +261 -0
- mcp_bridge/routing/task_classifier.py +190 -0
- mcp_bridge/server.py +363 -34
- mcp_bridge/server_tools.py +298 -6
- mcp_bridge/tools/__init__.py +19 -8
- mcp_bridge/tools/agent_manager.py +549 -799
- mcp_bridge/tools/background_tasks.py +13 -17
- mcp_bridge/tools/code_search.py +54 -51
- mcp_bridge/tools/continuous_loop.py +0 -1
- mcp_bridge/tools/dashboard.py +19 -0
- mcp_bridge/tools/find_code.py +296 -0
- mcp_bridge/tools/init.py +1 -0
- mcp_bridge/tools/list_directory.py +42 -0
- mcp_bridge/tools/lsp/__init__.py +8 -8
- mcp_bridge/tools/lsp/manager.py +51 -28
- mcp_bridge/tools/lsp/tools.py +98 -65
- mcp_bridge/tools/model_invoke.py +1047 -152
- mcp_bridge/tools/mux_client.py +75 -0
- mcp_bridge/tools/project_context.py +1 -2
- mcp_bridge/tools/query_classifier.py +132 -49
- mcp_bridge/tools/read_file.py +84 -0
- mcp_bridge/tools/replace.py +45 -0
- mcp_bridge/tools/run_shell_command.py +38 -0
- mcp_bridge/tools/search_enhancements.py +347 -0
- mcp_bridge/tools/semantic_search.py +677 -92
- mcp_bridge/tools/session_manager.py +0 -2
- mcp_bridge/tools/skill_loader.py +0 -1
- mcp_bridge/tools/task_runner.py +5 -7
- mcp_bridge/tools/templates.py +3 -3
- mcp_bridge/tools/tool_search.py +331 -0
- mcp_bridge/tools/write_file.py +29 -0
- mcp_bridge/update_manager.py +33 -37
- mcp_bridge/update_manager_pypi.py +6 -8
- mcp_bridge/utils/cache.py +82 -0
- mcp_bridge/utils/process.py +71 -0
- mcp_bridge/utils/session_state.py +51 -0
- mcp_bridge/utils/truncation.py +76 -0
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/METADATA +84 -35
- stravinsky-0.4.66.dist-info/RECORD +198 -0
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
- stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
- stravinsky_claude_assets/agents/HOOKS.md +437 -0
- stravinsky_claude_assets/agents/code-reviewer.md +210 -0
- stravinsky_claude_assets/agents/comment_checker.md +580 -0
- stravinsky_claude_assets/agents/debugger.md +254 -0
- stravinsky_claude_assets/agents/delphi.md +495 -0
- stravinsky_claude_assets/agents/dewey.md +248 -0
- stravinsky_claude_assets/agents/explore.md +1198 -0
- stravinsky_claude_assets/agents/frontend.md +472 -0
- stravinsky_claude_assets/agents/implementation-lead.md +164 -0
- stravinsky_claude_assets/agents/momus.md +464 -0
- stravinsky_claude_assets/agents/research-lead.md +141 -0
- stravinsky_claude_assets/agents/stravinsky.md +730 -0
- stravinsky_claude_assets/commands/delphi.md +9 -0
- stravinsky_claude_assets/commands/dewey.md +54 -0
- stravinsky_claude_assets/commands/git-master.md +112 -0
- stravinsky_claude_assets/commands/index.md +49 -0
- stravinsky_claude_assets/commands/publish.md +86 -0
- stravinsky_claude_assets/commands/review.md +73 -0
- stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
- stravinsky_claude_assets/commands/str/agent_list.md +56 -0
- stravinsky_claude_assets/commands/str/agent_output.md +92 -0
- stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
- stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
- stravinsky_claude_assets/commands/str/cancel.md +51 -0
- stravinsky_claude_assets/commands/str/clean.md +97 -0
- stravinsky_claude_assets/commands/str/continue.md +38 -0
- stravinsky_claude_assets/commands/str/index.md +199 -0
- stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
- stravinsky_claude_assets/commands/str/search.md +205 -0
- stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
- stravinsky_claude_assets/commands/str/stats.md +71 -0
- stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
- stravinsky_claude_assets/commands/str/unwatch.md +42 -0
- stravinsky_claude_assets/commands/str/watch.md +45 -0
- stravinsky_claude_assets/commands/strav.md +53 -0
- stravinsky_claude_assets/commands/stravinsky.md +292 -0
- stravinsky_claude_assets/commands/verify.md +60 -0
- stravinsky_claude_assets/commands/version.md +5 -0
- stravinsky_claude_assets/hooks/README.md +248 -0
- stravinsky_claude_assets/hooks/comment_checker.py +193 -0
- stravinsky_claude_assets/hooks/context.py +38 -0
- stravinsky_claude_assets/hooks/context_monitor.py +153 -0
- stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
- stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
- stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
- stravinsky_claude_assets/hooks/notification_hook.py +103 -0
- stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
- stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
- stravinsky_claude_assets/hooks/pre_compact.py +123 -0
- stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
- stravinsky_claude_assets/hooks/session_recovery.py +263 -0
- stravinsky_claude_assets/hooks/stop_hook.py +89 -0
- stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
- stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
- stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
- stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
- stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
- stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
- stravinsky_claude_assets/hooks/truncator.py +23 -0
- stravinsky_claude_assets/rules/deployment_safety.md +51 -0
- stravinsky_claude_assets/rules/integration_wiring.md +89 -0
- stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
- stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
- stravinsky_claude_assets/settings.json +152 -0
- stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
- stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
- stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
- stravinsky_claude_assets/task_dependencies.json +34 -0
- stravinsky-0.4.18.dist-info/RECORD +0 -88
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
|
@@ -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:
|
|
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'
|
|
66
|
-
r'\
|
|
67
|
-
r'
|
|
68
|
-
r'[\
|
|
69
|
-
r'
|
|
70
|
-
r'
|
|
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?|
|
|
79
|
-
r'\b(
|
|
80
|
-
r'
|
|
81
|
-
r'\b(
|
|
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'\
|
|
89
|
-
r'\
|
|
90
|
-
r'\
|
|
91
|
-
r'\b(
|
|
92
|
-
r'\b(
|
|
93
|
-
r'\
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
#
|
|
222
|
-
# -
|
|
223
|
-
# -
|
|
224
|
-
# -
|
|
225
|
-
# -
|
|
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:
|
|
228
|
-
QueryCategory.STRUCTURAL: len(structural_matches) * 0.
|
|
229
|
-
QueryCategory.SEMANTIC: len(semantic_matches) * 0.
|
|
230
|
-
QueryCategory.HYBRID: len(hybrid_matches) * 0.
|
|
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
|
-
#
|
|
331
|
+
# Tie-breaking logic
|
|
257
332
|
if len(winners) > 1:
|
|
258
333
|
confidence = min(max_score, 0.95)
|
|
259
|
-
|
|
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
|
|
267
|
-
all_indicators.
|
|
268
|
-
if
|
|
269
|
-
all_indicators.
|
|
270
|
-
if
|
|
271
|
-
all_indicators.
|
|
272
|
-
if
|
|
273
|
-
all_indicators.
|
|
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)}"
|