stravinsky 0.2.40__py3-none-any.whl → 0.3.4__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.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/auth/token_refresh.py +130 -0
- mcp_bridge/cli/__init__.py +6 -0
- mcp_bridge/cli/install_hooks.py +1265 -0
- mcp_bridge/cli/session_report.py +585 -0
- mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
- mcp_bridge/hooks/README.md +215 -0
- mcp_bridge/hooks/__init__.py +119 -43
- mcp_bridge/hooks/edit_recovery.py +42 -37
- mcp_bridge/hooks/git_noninteractive.py +89 -0
- mcp_bridge/hooks/keyword_detector.py +30 -0
- mcp_bridge/hooks/manager.py +50 -0
- mcp_bridge/hooks/notification_hook.py +103 -0
- mcp_bridge/hooks/parallel_enforcer.py +127 -0
- mcp_bridge/hooks/parallel_execution.py +111 -0
- mcp_bridge/hooks/pre_compact.py +123 -0
- mcp_bridge/hooks/preemptive_compaction.py +81 -7
- mcp_bridge/hooks/rules_injector.py +507 -0
- mcp_bridge/hooks/session_idle.py +116 -0
- mcp_bridge/hooks/session_notifier.py +125 -0
- mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
- mcp_bridge/hooks/subagent_stop.py +98 -0
- mcp_bridge/hooks/task_validator.py +73 -0
- mcp_bridge/hooks/tmux_manager.py +141 -0
- mcp_bridge/hooks/todo_continuation.py +90 -0
- mcp_bridge/hooks/todo_delegation.py +88 -0
- mcp_bridge/hooks/tool_messaging.py +164 -0
- mcp_bridge/hooks/truncator.py +21 -17
- mcp_bridge/notifications.py +151 -0
- mcp_bridge/prompts/__init__.py +3 -1
- mcp_bridge/prompts/dewey.py +30 -20
- mcp_bridge/prompts/explore.py +46 -8
- mcp_bridge/prompts/multimodal.py +24 -3
- mcp_bridge/prompts/planner.py +222 -0
- mcp_bridge/prompts/stravinsky.py +107 -28
- mcp_bridge/server.py +170 -10
- mcp_bridge/server_tools.py +554 -32
- mcp_bridge/tools/agent_manager.py +316 -106
- mcp_bridge/tools/background_tasks.py +2 -1
- mcp_bridge/tools/code_search.py +97 -11
- mcp_bridge/tools/lsp/__init__.py +7 -0
- mcp_bridge/tools/lsp/manager.py +448 -0
- mcp_bridge/tools/lsp/tools.py +637 -150
- mcp_bridge/tools/model_invoke.py +270 -47
- mcp_bridge/tools/semantic_search.py +2492 -0
- mcp_bridge/tools/templates.py +32 -18
- stravinsky-0.3.4.dist-info/METADATA +420 -0
- stravinsky-0.3.4.dist-info/RECORD +79 -0
- stravinsky-0.3.4.dist-info/entry_points.txt +5 -0
- mcp_bridge/native_hooks/edit_recovery.py +0 -46
- mcp_bridge/native_hooks/truncator.py +0 -23
- stravinsky-0.2.40.dist-info/METADATA +0 -204
- stravinsky-0.2.40.dist-info/RECORD +0 -57
- stravinsky-0.2.40.dist-info/entry_points.txt +0 -3
- /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
- {stravinsky-0.2.40.dist-info → stravinsky-0.3.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
UserPromptSubmit hook: Todo Continuation Enforcer
|
|
4
|
+
|
|
5
|
+
Checks if there are incomplete todos (in_progress or pending) and injects
|
|
6
|
+
a reminder to continue working on them before starting new work.
|
|
7
|
+
|
|
8
|
+
Aligned with oh-my-opencode's [SYSTEM REMINDER - TODO CONTINUATION] pattern.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_todo_state() -> dict:
|
|
17
|
+
"""Try to get current todo state from Claude Code session or local cache."""
|
|
18
|
+
# Claude Code stores todo state - we can check via session files
|
|
19
|
+
# For now, we'll use a simple file-based approach
|
|
20
|
+
cwd = Path(os.environ.get("CLAUDE_CWD", "."))
|
|
21
|
+
todo_cache = cwd / ".claude" / "todo_state.json"
|
|
22
|
+
|
|
23
|
+
if todo_cache.exists():
|
|
24
|
+
try:
|
|
25
|
+
return json.loads(todo_cache.read_text())
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
return {"todos": []}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main():
|
|
33
|
+
try:
|
|
34
|
+
data = json.load(sys.stdin)
|
|
35
|
+
prompt = data.get("prompt", "")
|
|
36
|
+
except Exception:
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
# Get current todo state
|
|
40
|
+
state = get_todo_state()
|
|
41
|
+
todos = state.get("todos", [])
|
|
42
|
+
|
|
43
|
+
if not todos:
|
|
44
|
+
# No todos tracked, pass through
|
|
45
|
+
print(prompt)
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
# Count incomplete todos
|
|
49
|
+
in_progress = [t for t in todos if t.get("status") == "in_progress"]
|
|
50
|
+
pending = [t for t in todos if t.get("status") == "pending"]
|
|
51
|
+
|
|
52
|
+
if not in_progress and not pending:
|
|
53
|
+
# All todos complete, pass through
|
|
54
|
+
print(prompt)
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
# Build reminder
|
|
58
|
+
reminder_parts = ["[SYSTEM REMINDER - TODO CONTINUATION]", ""]
|
|
59
|
+
|
|
60
|
+
if in_progress:
|
|
61
|
+
reminder_parts.append(f"IN PROGRESS ({len(in_progress)} items):")
|
|
62
|
+
for t in in_progress:
|
|
63
|
+
reminder_parts.append(f" - {t.get('content', 'Unknown task')}")
|
|
64
|
+
reminder_parts.append("")
|
|
65
|
+
|
|
66
|
+
if pending:
|
|
67
|
+
reminder_parts.append(f"PENDING ({len(pending)} items):")
|
|
68
|
+
for t in pending[:5]: # Show max 5 pending
|
|
69
|
+
reminder_parts.append(f" - {t.get('content', 'Unknown task')}")
|
|
70
|
+
if len(pending) > 5:
|
|
71
|
+
reminder_parts.append(f" ... and {len(pending) - 5} more")
|
|
72
|
+
reminder_parts.append("")
|
|
73
|
+
|
|
74
|
+
reminder_parts.extend([
|
|
75
|
+
"IMPORTANT: You have incomplete work. Before starting anything new:",
|
|
76
|
+
"1. Continue working on IN_PROGRESS todos first",
|
|
77
|
+
"2. If blocked, explain why and move to next PENDING item",
|
|
78
|
+
"3. Only start NEW work if all todos are complete or explicitly abandoned",
|
|
79
|
+
"",
|
|
80
|
+
"---",
|
|
81
|
+
"",
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
reminder = "\n".join(reminder_parts)
|
|
85
|
+
print(reminder + prompt)
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
sys.exit(main())
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse hook for TodoWrite: CRITICAL parallel execution enforcer.
|
|
4
|
+
|
|
5
|
+
This hook fires AFTER TodoWrite completes. If there are 2+ pending items,
|
|
6
|
+
it outputs a STRONG reminder that Task agents must be spawned immediately.
|
|
7
|
+
|
|
8
|
+
Exit code 2 is used to signal a HARD BLOCK - Claude should see this as
|
|
9
|
+
a failure condition requiring immediate correction.
|
|
10
|
+
|
|
11
|
+
Works in tandem with:
|
|
12
|
+
- parallel_execution.py (UserPromptSubmit): Pre-emptive instruction injection
|
|
13
|
+
- stravinsky_mode.py (PreToolUse): Hard blocking of Read/Grep/Bash tools
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# Check if stravinsky mode is active (hard blocking enabled)
|
|
20
|
+
STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_stravinsky_mode():
|
|
24
|
+
"""Check if hard blocking mode is active."""
|
|
25
|
+
return STRAVINSKY_MODE_FILE.exists()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
# Read hook input from stdin
|
|
30
|
+
try:
|
|
31
|
+
hook_input = json.load(sys.stdin)
|
|
32
|
+
except (json.JSONDecodeError, EOFError):
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
tool_name = hook_input.get("tool_name", "")
|
|
36
|
+
|
|
37
|
+
if tool_name != "TodoWrite":
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
# Get the todos that were just written
|
|
41
|
+
tool_input = hook_input.get("tool_input", {})
|
|
42
|
+
todos = tool_input.get("todos", [])
|
|
43
|
+
|
|
44
|
+
# Count pending todos
|
|
45
|
+
pending_count = sum(1 for t in todos if t.get("status") == "pending")
|
|
46
|
+
|
|
47
|
+
if pending_count < 2:
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
# Check if stravinsky mode is active
|
|
51
|
+
stravinsky_active = is_stravinsky_mode()
|
|
52
|
+
|
|
53
|
+
# CRITICAL: Output urgent reminder for parallel Task spawning
|
|
54
|
+
mode_warning = ""
|
|
55
|
+
if stravinsky_active:
|
|
56
|
+
mode_warning = """
|
|
57
|
+
⚠️ STRAVINSKY MODE ACTIVE - Direct tools (Read, Grep, Bash) are BLOCKED.
|
|
58
|
+
You MUST use Task(subagent_type="explore", ...) for ALL file operations.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
error_message = f"""
|
|
62
|
+
🚨 PARALLEL DELEGATION REQUIRED 🚨
|
|
63
|
+
|
|
64
|
+
TodoWrite created {pending_count} pending items.
|
|
65
|
+
{mode_warning}
|
|
66
|
+
You MUST spawn Task agents for ALL independent TODOs in THIS SAME RESPONSE.
|
|
67
|
+
|
|
68
|
+
Required pattern (IMMEDIATELY after this message):
|
|
69
|
+
Task(subagent_type="explore", prompt="TODO 1...", description="TODO 1", run_in_background=true)
|
|
70
|
+
Task(subagent_type="explore", prompt="TODO 2...", description="TODO 2", run_in_background=true)
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
DO NOT:
|
|
74
|
+
- End your response without spawning Tasks
|
|
75
|
+
- Mark TODOs in_progress before spawning Tasks
|
|
76
|
+
- Use Read/Grep/Bash directly (BLOCKED in stravinsky mode)
|
|
77
|
+
|
|
78
|
+
Your NEXT action MUST be multiple Task() calls, one for each independent TODO.
|
|
79
|
+
"""
|
|
80
|
+
print(error_message, file=sys.stderr)
|
|
81
|
+
|
|
82
|
+
# Exit code 2 = HARD BLOCK in stravinsky mode
|
|
83
|
+
# Exit code 1 = WARNING otherwise
|
|
84
|
+
return 2 if stravinsky_active else 1
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
sys.exit(main())
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse hook for user-friendly tool messaging.
|
|
4
|
+
|
|
5
|
+
Outputs concise messages about which agent/tool was used and what it did.
|
|
6
|
+
Format examples:
|
|
7
|
+
- ast-grep('Searching for authentication patterns')
|
|
8
|
+
- delphi:openai/gpt-5.2-medium('Analyzing architecture trade-offs')
|
|
9
|
+
- explore:gemini-3-flash('Finding all API endpoints')
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
# Agent model mappings
|
|
17
|
+
AGENT_MODELS = {
|
|
18
|
+
"explore": "gemini-3-flash",
|
|
19
|
+
"dewey": "gemini-3-flash",
|
|
20
|
+
"code-reviewer": "sonnet",
|
|
21
|
+
"debugger": "sonnet",
|
|
22
|
+
"frontend": "gemini-3-pro-high",
|
|
23
|
+
"delphi": "gpt-5.2-medium",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Tool display names
|
|
27
|
+
TOOL_NAMES = {
|
|
28
|
+
"mcp__stravinsky__ast_grep_search": "ast-grep",
|
|
29
|
+
"mcp__stravinsky__grep_search": "grep",
|
|
30
|
+
"mcp__stravinsky__glob_files": "glob",
|
|
31
|
+
"mcp__stravinsky__lsp_diagnostics": "lsp-diagnostics",
|
|
32
|
+
"mcp__stravinsky__lsp_hover": "lsp-hover",
|
|
33
|
+
"mcp__stravinsky__lsp_goto_definition": "lsp-goto-def",
|
|
34
|
+
"mcp__stravinsky__lsp_find_references": "lsp-find-refs",
|
|
35
|
+
"mcp__stravinsky__lsp_document_symbols": "lsp-symbols",
|
|
36
|
+
"mcp__stravinsky__lsp_workspace_symbols": "lsp-workspace-symbols",
|
|
37
|
+
"mcp__stravinsky__invoke_gemini": "gemini",
|
|
38
|
+
"mcp__stravinsky__invoke_openai": "openai",
|
|
39
|
+
"mcp__grep-app__searchCode": "grep.app",
|
|
40
|
+
"mcp__grep-app__github_file": "github-file",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extract_description(tool_name: str, params: dict) -> str:
|
|
45
|
+
"""Extract a concise description of what the tool did."""
|
|
46
|
+
|
|
47
|
+
# AST-grep
|
|
48
|
+
if "ast_grep" in tool_name:
|
|
49
|
+
pattern = params.get("pattern", "")
|
|
50
|
+
directory = params.get("directory", ".")
|
|
51
|
+
return f"Searching AST in {directory} for '{pattern[:40]}...'"
|
|
52
|
+
|
|
53
|
+
# Grep/search
|
|
54
|
+
if "grep_search" in tool_name or "searchCode" in tool_name:
|
|
55
|
+
pattern = params.get("pattern", params.get("query", ""))
|
|
56
|
+
return f"Searching for '{pattern[:40]}...'"
|
|
57
|
+
|
|
58
|
+
# Glob
|
|
59
|
+
if "glob_files" in tool_name:
|
|
60
|
+
pattern = params.get("pattern", "")
|
|
61
|
+
return f"Finding files matching '{pattern}'"
|
|
62
|
+
|
|
63
|
+
# LSP diagnostics
|
|
64
|
+
if "lsp_diagnostics" in tool_name:
|
|
65
|
+
file_path = params.get("file_path", "")
|
|
66
|
+
filename = os.path.basename(file_path) if file_path else "file"
|
|
67
|
+
return f"Checking {filename} for errors"
|
|
68
|
+
|
|
69
|
+
# LSP hover
|
|
70
|
+
if "lsp_hover" in tool_name:
|
|
71
|
+
file_path = params.get("file_path", "")
|
|
72
|
+
line = params.get("line", "")
|
|
73
|
+
filename = os.path.basename(file_path) if file_path else "file"
|
|
74
|
+
return f"Type info for {filename}:{line}"
|
|
75
|
+
|
|
76
|
+
# LSP goto definition
|
|
77
|
+
if "lsp_goto" in tool_name:
|
|
78
|
+
file_path = params.get("file_path", "")
|
|
79
|
+
filename = os.path.basename(file_path) if file_path else "symbol"
|
|
80
|
+
return f"Finding definition in {filename}"
|
|
81
|
+
|
|
82
|
+
# LSP find references
|
|
83
|
+
if "lsp_find_references" in tool_name:
|
|
84
|
+
file_path = params.get("file_path", "")
|
|
85
|
+
filename = os.path.basename(file_path) if file_path else "symbol"
|
|
86
|
+
return f"Finding all references to symbol in {filename}"
|
|
87
|
+
|
|
88
|
+
# LSP symbols
|
|
89
|
+
if "lsp_symbols" in tool_name or "lsp_document_symbols" in tool_name:
|
|
90
|
+
file_path = params.get("file_path", "")
|
|
91
|
+
filename = os.path.basename(file_path) if file_path else "file"
|
|
92
|
+
return f"Getting symbols from {filename}"
|
|
93
|
+
|
|
94
|
+
if "lsp_workspace_symbols" in tool_name:
|
|
95
|
+
query = params.get("query", "")
|
|
96
|
+
return f"Searching workspace for symbol '{query}'"
|
|
97
|
+
|
|
98
|
+
# Gemini invocation
|
|
99
|
+
if "invoke_gemini" in tool_name:
|
|
100
|
+
prompt = params.get("prompt", "")
|
|
101
|
+
# Extract first meaningful line
|
|
102
|
+
first_line = prompt.split('\n')[0][:50] if prompt else "Processing"
|
|
103
|
+
return first_line
|
|
104
|
+
|
|
105
|
+
# OpenAI invocation
|
|
106
|
+
if "invoke_openai" in tool_name:
|
|
107
|
+
prompt = params.get("prompt", "")
|
|
108
|
+
first_line = prompt.split('\n')[0][:50] if prompt else "Strategic analysis"
|
|
109
|
+
return first_line
|
|
110
|
+
|
|
111
|
+
# GitHub file fetch
|
|
112
|
+
if "github_file" in tool_name:
|
|
113
|
+
path = params.get("path", "")
|
|
114
|
+
repo = params.get("repo", "")
|
|
115
|
+
return f"Fetching {path} from {repo}"
|
|
116
|
+
|
|
117
|
+
# Task delegation
|
|
118
|
+
if tool_name == "Task":
|
|
119
|
+
subagent_type = params.get("subagent_type", "unknown")
|
|
120
|
+
description = params.get("description", "")
|
|
121
|
+
model = AGENT_MODELS.get(subagent_type, "unknown")
|
|
122
|
+
return f"{subagent_type}:{model}('{description}')"
|
|
123
|
+
|
|
124
|
+
return "Processing"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main():
|
|
128
|
+
try:
|
|
129
|
+
# Read hook input from stdin
|
|
130
|
+
hook_input = json.loads(sys.stdin.read())
|
|
131
|
+
|
|
132
|
+
tool_name = hook_input.get("toolName", hook_input.get("tool_name", ""))
|
|
133
|
+
params = hook_input.get("params", hook_input.get("tool_input", {}))
|
|
134
|
+
|
|
135
|
+
# Only output messages for MCP tools and Task delegations
|
|
136
|
+
if not (tool_name.startswith("mcp__") or tool_name == "Task"):
|
|
137
|
+
sys.exit(0)
|
|
138
|
+
|
|
139
|
+
# Get tool display name
|
|
140
|
+
display_name = TOOL_NAMES.get(tool_name, tool_name)
|
|
141
|
+
|
|
142
|
+
# Special handling for Task delegations
|
|
143
|
+
if tool_name == "Task":
|
|
144
|
+
subagent_type = params.get("subagent_type", "unknown")
|
|
145
|
+
description = params.get("description", "")
|
|
146
|
+
model = AGENT_MODELS.get(subagent_type, "unknown")
|
|
147
|
+
|
|
148
|
+
# Show full agent delegation message
|
|
149
|
+
print(f"🎯 {subagent_type}:{model}('{description}')", file=sys.stderr)
|
|
150
|
+
else:
|
|
151
|
+
# Regular tool usage
|
|
152
|
+
description = extract_description(tool_name, params)
|
|
153
|
+
print(f"🔧 {display_name}('{description}')", file=sys.stderr)
|
|
154
|
+
|
|
155
|
+
sys.exit(0)
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
# On error, fail silently (don't disrupt workflow)
|
|
159
|
+
print(f"Tool messaging hook error: {e}", file=sys.stderr)
|
|
160
|
+
sys.exit(0)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
main()
|
mcp_bridge/hooks/truncator.py
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
"""
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
MAX_CHARS = 30000
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
def main():
|
|
8
|
+
try:
|
|
9
|
+
data = json.load(sys.stdin)
|
|
10
|
+
tool_response = data.get("tool_response", "")
|
|
11
|
+
except Exception:
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
if len(tool_response) > MAX_CHARS:
|
|
15
|
+
header = f"[TRUNCATED - {len(tool_response)} chars reduced to {MAX_CHARS}]\n"
|
|
16
|
+
footer = "\n...[TRUNCATED]"
|
|
17
|
+
truncated = tool_response[:MAX_CHARS]
|
|
18
|
+
print(header + truncated + footer)
|
|
19
|
+
else:
|
|
20
|
+
print(tool_response)
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Desktop Notifications Manager for Stravinsky.
|
|
3
|
+
|
|
4
|
+
Provides cross-platform desktop notifications (macOS, Linux, Windows)
|
|
5
|
+
for long-running operations like codebase indexing.
|
|
6
|
+
|
|
7
|
+
Supports:
|
|
8
|
+
- Non-blocking async notifications
|
|
9
|
+
- Platform-specific backends
|
|
10
|
+
- Notification queuing
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import platform
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Dict, Optional
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotificationManager:
|
|
23
|
+
"""
|
|
24
|
+
Cross-platform desktop notification manager.
|
|
25
|
+
|
|
26
|
+
Provides non-blocking notifications with automatic platform detection.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, app_name: str = "Stravinsky"):
|
|
30
|
+
self.app_name = app_name
|
|
31
|
+
self.system = platform.system()
|
|
32
|
+
|
|
33
|
+
def _get_notification_command(
|
|
34
|
+
self,
|
|
35
|
+
title: str,
|
|
36
|
+
message: str,
|
|
37
|
+
sound: bool = True
|
|
38
|
+
) -> Optional[list]:
|
|
39
|
+
"""Get platform-specific notification command."""
|
|
40
|
+
if self.system == "Darwin": # macOS
|
|
41
|
+
script = f'display notification "{message}" with title "{title}"'
|
|
42
|
+
if sound:
|
|
43
|
+
script += ' sound name "Glass"'
|
|
44
|
+
return ["osascript", "-e", script]
|
|
45
|
+
|
|
46
|
+
elif self.system == "Linux":
|
|
47
|
+
cmd = ["notify-send", "--app-name", self.app_name, title, message]
|
|
48
|
+
if sound:
|
|
49
|
+
cmd.extend(["--urgency=normal"])
|
|
50
|
+
return cmd
|
|
51
|
+
|
|
52
|
+
elif self.system == "Windows":
|
|
53
|
+
ps_script = f"""
|
|
54
|
+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
55
|
+
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
56
|
+
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
|
57
|
+
|
|
58
|
+
$template = @"
|
|
59
|
+
<toast>
|
|
60
|
+
<visual>
|
|
61
|
+
<binding template="ToastGeneric">
|
|
62
|
+
<text>{title}</text>
|
|
63
|
+
<text>{message}</text>
|
|
64
|
+
</binding>
|
|
65
|
+
</visual>
|
|
66
|
+
</toast>
|
|
67
|
+
"@
|
|
68
|
+
|
|
69
|
+
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
70
|
+
$xml.LoadXml($template)
|
|
71
|
+
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
|
|
72
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("{self.app_name}").Show($toast)
|
|
73
|
+
"""
|
|
74
|
+
return ["powershell", "-Command", ps_script]
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def _send_notification_sync(
|
|
79
|
+
self,
|
|
80
|
+
title: str,
|
|
81
|
+
message: str,
|
|
82
|
+
sound: bool = True
|
|
83
|
+
) -> bool:
|
|
84
|
+
"""Send notification synchronously (blocking)."""
|
|
85
|
+
cmd = self._get_notification_command(title, message, sound)
|
|
86
|
+
|
|
87
|
+
if not cmd:
|
|
88
|
+
logger.warning(
|
|
89
|
+
f"[Notifications] Desktop notifications not supported on {self.system}"
|
|
90
|
+
)
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
subprocess.Popen(
|
|
95
|
+
cmd,
|
|
96
|
+
stdout=subprocess.DEVNULL,
|
|
97
|
+
stderr=subprocess.DEVNULL,
|
|
98
|
+
start_new_session=True
|
|
99
|
+
)
|
|
100
|
+
logger.debug(f"[Notifications] Sent: {title}")
|
|
101
|
+
return True
|
|
102
|
+
except FileNotFoundError:
|
|
103
|
+
logger.warning(f"[Notifications] Command not found: {cmd[0]}")
|
|
104
|
+
return False
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"[Notifications] Failed to send notification: {e}")
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
async def notify_reindex_start(self, project_path: str) -> bool:
|
|
110
|
+
"""Notify that codebase reindexing has started."""
|
|
111
|
+
path = Path(project_path).name or Path(project_path).parent.name
|
|
112
|
+
title = "Codebase Indexing Started"
|
|
113
|
+
message = f"Indexing {path}..."
|
|
114
|
+
return self._send_notification_sync(title, message, sound=True)
|
|
115
|
+
|
|
116
|
+
async def notify_reindex_complete(self, stats: Dict) -> bool:
|
|
117
|
+
"""Notify that codebase reindexing is complete."""
|
|
118
|
+
indexed = stats.get("indexed", 0)
|
|
119
|
+
pruned = stats.get("pruned", 0)
|
|
120
|
+
time_taken = stats.get("time_taken", 0)
|
|
121
|
+
|
|
122
|
+
title = "Codebase Indexing Complete"
|
|
123
|
+
message = f"Indexed {indexed} chunks, pruned {pruned} stale entries in {time_taken}s"
|
|
124
|
+
|
|
125
|
+
return self._send_notification_sync(title, message, sound=True)
|
|
126
|
+
|
|
127
|
+
async def notify_reindex_error(self, error_message: str) -> bool:
|
|
128
|
+
"""Notify that codebase reindexing failed."""
|
|
129
|
+
title = "Codebase Indexing Failed"
|
|
130
|
+
# Truncate long error messages
|
|
131
|
+
message = error_message[:100] + "..." if len(error_message) > 100 else error_message
|
|
132
|
+
|
|
133
|
+
return self._send_notification_sync(title, message, sound=True)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Global singleton instance
|
|
137
|
+
_notification_manager: Optional[NotificationManager] = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_notification_manager() -> NotificationManager:
|
|
141
|
+
"""Get or create the global notification manager instance."""
|
|
142
|
+
global _notification_manager
|
|
143
|
+
if _notification_manager is None:
|
|
144
|
+
_notification_manager = NotificationManager()
|
|
145
|
+
return _notification_manager
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def reset_notification_manager() -> None:
|
|
149
|
+
"""Reset the global notification manager (for testing)."""
|
|
150
|
+
global _notification_manager
|
|
151
|
+
_notification_manager = None
|
mcp_bridge/prompts/__init__.py
CHANGED
|
@@ -6,13 +6,15 @@ from . import explore
|
|
|
6
6
|
from . import frontend
|
|
7
7
|
from . import document_writer
|
|
8
8
|
from . import multimodal
|
|
9
|
+
from . import planner
|
|
9
10
|
|
|
10
11
|
__all__ = [
|
|
11
12
|
"stravinsky",
|
|
12
|
-
"delphi",
|
|
13
|
+
"delphi",
|
|
13
14
|
"dewey",
|
|
14
15
|
"explore",
|
|
15
16
|
"frontend",
|
|
16
17
|
"document_writer",
|
|
17
18
|
"multimodal",
|
|
19
|
+
"planner",
|
|
18
20
|
]
|
mcp_bridge/prompts/dewey.py
CHANGED
|
@@ -50,8 +50,8 @@ Classify EVERY request into one of these categories before taking action:
|
|
|
50
50
|
|
|
51
51
|
| Type | Trigger Examples | Tools |
|
|
52
52
|
|------|------------------|-------|
|
|
53
|
-
| **TYPE A: CONCEPTUAL** | "How do I use X?", "Best practice for Y?" |
|
|
54
|
-
| **TYPE B: IMPLEMENTATION** | "How does X implement Y?", "Show me source of Z" | gh clone + read + blame |
|
|
53
|
+
| **TYPE A: CONCEPTUAL** | "How do I use X?", "Best practice for Y?" | exa websearch + grep-app GitHub search (parallel) |
|
|
54
|
+
| **TYPE B: IMPLEMENTATION** | "How does X implement Y?", "Show me source of Z" | gh clone + ast-grep + read + blame |
|
|
55
55
|
| **TYPE C: CONTEXT** | "Why was this changed?", "History of X?" | gh issues/prs + git log/blame |
|
|
56
56
|
| **TYPE D: COMPREHENSIVE** | Complex/ambiguous requests | ALL tools in parallel |
|
|
57
57
|
|
|
@@ -64,12 +64,15 @@ Classify EVERY request into one of these categories before taking action:
|
|
|
64
64
|
|
|
65
65
|
**Execute in parallel (3+ calls)**:
|
|
66
66
|
```
|
|
67
|
-
Tool 1:
|
|
68
|
-
|
|
69
|
-
Tool
|
|
67
|
+
Tool 1: mcp__MCP_DOCKER__web_search_exa(query="library-name topic 2026", num_results=5)
|
|
68
|
+
-> Current articles, blog posts, best practices (ALWAYS use Exa instead of native WebSearch)
|
|
69
|
+
Tool 2: mcp__grep-app__searchCode(query="library-name implementation pattern")
|
|
70
|
+
-> Real GitHub code examples with permalinks
|
|
71
|
+
Tool 3: gh search repos "library-name" --sort stars --limit 5
|
|
72
|
+
-> Popular repositories for reference
|
|
70
73
|
```
|
|
71
74
|
|
|
72
|
-
**Output**:
|
|
75
|
+
**Output**: Synthesize with evidence links (Exa URLs + GitHub permalinks).
|
|
73
76
|
|
|
74
77
|
---
|
|
75
78
|
|
|
@@ -85,8 +88,8 @@ Step 2: Get commit SHA for permalinks
|
|
|
85
88
|
cd ${TMPDIR:-/tmp}/repo-name && git rev-parse HEAD
|
|
86
89
|
|
|
87
90
|
Step 3: Find the implementation
|
|
88
|
-
-
|
|
89
|
-
-
|
|
91
|
+
- mcp__ast-grep__find_code(pattern="function $NAME", language="typescript") for structural search
|
|
92
|
+
- grep_search for function/class names
|
|
90
93
|
- Read the specific file
|
|
91
94
|
- git blame for context if needed
|
|
92
95
|
|
|
@@ -97,9 +100,9 @@ Step 4: Construct permalink
|
|
|
97
100
|
**Parallel acceleration (4+ calls)**:
|
|
98
101
|
```
|
|
99
102
|
Tool 1: gh repo clone owner/repo ${TMPDIR:-/tmp}/repo -- --depth 1
|
|
100
|
-
Tool 2:
|
|
103
|
+
Tool 2: mcp__grep-app__searchCode(query="repo:owner/repo function_name")
|
|
101
104
|
Tool 3: gh api repos/owner/repo/commits/HEAD --jq '.sha'
|
|
102
|
-
Tool 4:
|
|
105
|
+
Tool 4: mcp__MCP_DOCKER__web_search_exa(query="library-name function_name documentation 2026")
|
|
103
106
|
```
|
|
104
107
|
|
|
105
108
|
---
|
|
@@ -131,13 +134,15 @@ gh api repos/owner/repo/pulls/<number>/files
|
|
|
131
134
|
|
|
132
135
|
**Execute ALL in parallel (6+ calls)**:
|
|
133
136
|
```
|
|
134
|
-
//
|
|
135
|
-
Tool 1:
|
|
136
|
-
Tool 2: Web search ("topic recent updates 2025")
|
|
137
|
+
// Web Search (ALWAYS use Exa)
|
|
138
|
+
Tool 1: mcp__MCP_DOCKER__web_search_exa(query="topic recent updates 2026", num_results=10)
|
|
137
139
|
|
|
138
|
-
// Code Search
|
|
139
|
-
Tool
|
|
140
|
-
Tool
|
|
140
|
+
// GitHub Code Search
|
|
141
|
+
Tool 2: mcp__grep-app__searchCode(query="topic implementation pattern")
|
|
142
|
+
Tool 3: mcp__grep-app__searchCode(query="topic usage example")
|
|
143
|
+
|
|
144
|
+
// AST Pattern Search
|
|
145
|
+
Tool 4: mcp__ast-grep__find_code(pattern="$PATTERN", language="typescript")
|
|
141
146
|
|
|
142
147
|
// Source Analysis
|
|
143
148
|
Tool 5: gh repo clone owner/repo ${TMPDIR:-/tmp}/repo -- --depth 1
|
|
@@ -182,15 +187,20 @@ https://github.com/tanstack/query/blob/abc123def/packages/react-query/src/useQue
|
|
|
182
187
|
|
|
183
188
|
---
|
|
184
189
|
|
|
185
|
-
## TOOL REFERENCE (Stravinsky Tools)
|
|
190
|
+
## TOOL REFERENCE (Stravinsky + MCP DOCKER Tools)
|
|
186
191
|
|
|
187
192
|
### Primary Tools by Purpose
|
|
188
193
|
|
|
189
194
|
| Purpose | Tool | Usage |
|
|
190
195
|
|---------|------|-------|
|
|
191
|
-
| **
|
|
192
|
-
| **
|
|
193
|
-
| **File
|
|
196
|
+
| **Web Search** | `mcp__MCP_DOCKER__web_search_exa` | **ALWAYS use instead of native WebSearch** - Real-time web search for current articles, docs, tutorials |
|
|
197
|
+
| **GitHub Code Search** | `mcp__grep-app__searchCode` | Search across public GitHub repositories - returns permalinks |
|
|
198
|
+
| **GitHub File Fetch** | `mcp__grep-app__github_file` | Fetch specific file from GitHub repo |
|
|
199
|
+
| **AST Pattern Search** | `mcp__ast-grep__find_code` | Structural code search across 25+ languages with AST awareness |
|
|
200
|
+
| **AST Replace** | `mcp__ast-grep__replace` | AST-aware code refactoring and replacement |
|
|
201
|
+
| **Local Code Search** | `grep_search` | Pattern-based search in local/cloned repos (uses ripgrep) |
|
|
202
|
+
| **Local AST Search** | `ast_grep_search` | AST search in cloned repos |
|
|
203
|
+
| **File Glob** | `glob_files` | Find files by pattern |
|
|
194
204
|
| **Clone Repo** | gh CLI | `gh repo clone owner/repo ${TMPDIR:-/tmp}/name -- --depth 1` |
|
|
195
205
|
| **Issues/PRs** | gh CLI | `gh search issues/prs "query" --repo owner/repo` |
|
|
196
206
|
| **View Issue/PR** | gh CLI | `gh issue/pr view <num> --repo owner/repo --comments` |
|