stravinsky 0.2.7__py3-none-any.whl → 0.2.40__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.
Files changed (34) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/cli.py +84 -46
  3. mcp_bridge/auth/oauth.py +88 -63
  4. mcp_bridge/hooks/__init__.py +29 -8
  5. mcp_bridge/hooks/agent_reminder.py +61 -0
  6. mcp_bridge/hooks/auto_slash_command.py +186 -0
  7. mcp_bridge/hooks/comment_checker.py +136 -0
  8. mcp_bridge/hooks/context_monitor.py +58 -0
  9. mcp_bridge/hooks/empty_message_sanitizer.py +240 -0
  10. mcp_bridge/hooks/keyword_detector.py +122 -0
  11. mcp_bridge/hooks/manager.py +27 -8
  12. mcp_bridge/hooks/preemptive_compaction.py +157 -0
  13. mcp_bridge/hooks/session_recovery.py +186 -0
  14. mcp_bridge/hooks/todo_enforcer.py +75 -0
  15. mcp_bridge/hooks/truncator.py +1 -1
  16. mcp_bridge/native_hooks/stravinsky_mode.py +109 -0
  17. mcp_bridge/native_hooks/truncator.py +1 -1
  18. mcp_bridge/prompts/delphi.py +3 -2
  19. mcp_bridge/prompts/dewey.py +105 -21
  20. mcp_bridge/prompts/stravinsky.py +451 -127
  21. mcp_bridge/server.py +304 -38
  22. mcp_bridge/server_tools.py +21 -3
  23. mcp_bridge/tools/__init__.py +2 -1
  24. mcp_bridge/tools/agent_manager.py +313 -236
  25. mcp_bridge/tools/init.py +1 -1
  26. mcp_bridge/tools/model_invoke.py +534 -52
  27. mcp_bridge/tools/skill_loader.py +51 -47
  28. mcp_bridge/tools/task_runner.py +74 -30
  29. mcp_bridge/tools/templates.py +101 -12
  30. {stravinsky-0.2.7.dist-info → stravinsky-0.2.40.dist-info}/METADATA +6 -12
  31. stravinsky-0.2.40.dist-info/RECORD +57 -0
  32. stravinsky-0.2.7.dist-info/RECORD +0 -47
  33. {stravinsky-0.2.7.dist-info → stravinsky-0.2.40.dist-info}/WHEEL +0 -0
  34. {stravinsky-0.2.7.dist-info → stravinsky-0.2.40.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Hooks initialization.
3
- Registers all Tier 1-3 hooks into the HookManager.
3
+ Registers all Tier 1-4 hooks into the HookManager.
4
4
  """
5
5
 
6
6
  from .manager import get_hook_manager
@@ -9,20 +9,41 @@ from .edit_recovery import edit_error_recovery_hook
9
9
  from .directory_context import directory_context_hook
10
10
  from .compaction import context_compaction_hook
11
11
  from .budget_optimizer import budget_optimizer_hook
12
+ from .todo_enforcer import todo_continuation_hook
13
+ from .keyword_detector import keyword_detector_hook
14
+ from .comment_checker import comment_checker_hook
15
+ from .context_monitor import context_monitor_hook
16
+ from .agent_reminder import agent_reminder_hook
17
+ from .preemptive_compaction import preemptive_compaction_hook
18
+ from .auto_slash_command import auto_slash_command_hook
19
+ from .session_recovery import session_recovery_hook
20
+ from .empty_message_sanitizer import empty_message_sanitizer_hook
21
+
12
22
 
13
23
  def initialize_hooks():
14
24
  """Register all available hooks."""
15
25
  manager = get_hook_manager()
16
-
17
- # Tier 1
26
+
27
+ # Tier 1: Post-tool-call (immediate response modification)
18
28
  manager.register_post_tool_call(output_truncator_hook)
19
29
  manager.register_post_tool_call(edit_error_recovery_hook)
20
-
21
- # Tier 2
30
+ manager.register_post_tool_call(comment_checker_hook)
31
+ manager.register_post_tool_call(agent_reminder_hook)
32
+ manager.register_post_tool_call(session_recovery_hook)
33
+
34
+ # Tier 2: Pre-model-invoke (context management)
22
35
  manager.register_pre_model_invoke(directory_context_hook)
23
36
  manager.register_pre_model_invoke(context_compaction_hook)
24
-
25
- # Tier 3
37
+ manager.register_pre_model_invoke(context_monitor_hook)
38
+ manager.register_pre_model_invoke(preemptive_compaction_hook)
39
+ manager.register_pre_model_invoke(empty_message_sanitizer_hook)
40
+
41
+ # Tier 3: Pre-model-invoke (performance optimization)
26
42
  manager.register_pre_model_invoke(budget_optimizer_hook)
27
43
 
28
- # initialize_hooks()
44
+ # Tier 4: Pre-model-invoke (behavior enforcement)
45
+ manager.register_pre_model_invoke(keyword_detector_hook)
46
+ manager.register_pre_model_invoke(todo_continuation_hook)
47
+ manager.register_pre_model_invoke(auto_slash_command_hook)
48
+
49
+ return manager
@@ -0,0 +1,61 @@
1
+ """
2
+ Agent Usage Reminder Hook.
3
+
4
+ When direct search tools (grep, glob, find) are used,
5
+ suggests using background agents for more comprehensive results.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict, Optional
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ AGENT_SUGGESTION = """
14
+ [AGENT SUGGESTION]
15
+ You used `{tool_name}` directly. For more comprehensive results, consider:
16
+
17
+ **Background Agents (parallel, more thorough):**
18
+ ```
19
+ background_task(agent="explore", prompt="Search for {search_context}...")
20
+ background_task(agent="dewey", prompt="Find documentation for {search_context}...")
21
+ ```
22
+
23
+ Background agents can search multiple patterns simultaneously and provide richer context.
24
+ Use direct tools for quick, targeted lookups. Use agents for exploratory research.
25
+ """
26
+
27
+ SEARCH_TOOLS = {"grep", "glob", "rg", "find", "Grep", "Glob", "grep_search", "glob_files"}
28
+
29
+
30
+ async def agent_reminder_hook(
31
+ tool_name: str, arguments: Dict[str, Any], output: str
32
+ ) -> Optional[str]:
33
+ """
34
+ Post-tool call hook that suggests background agents after direct search tool usage.
35
+ """
36
+ if tool_name not in SEARCH_TOOLS:
37
+ return None
38
+
39
+ search_context = _extract_search_context(arguments)
40
+
41
+ if not search_context:
42
+ return None
43
+
44
+ if len(output) < 100:
45
+ logger.info(
46
+ f"[AgentReminder] Direct search '{tool_name}' returned limited results, suggesting agents"
47
+ )
48
+ suggestion = AGENT_SUGGESTION.format(tool_name=tool_name, search_context=search_context)
49
+ return output + "\n" + suggestion
50
+
51
+ return None
52
+
53
+
54
+ def _extract_search_context(arguments: Dict[str, Any]) -> str:
55
+ """Extract search context from tool arguments."""
56
+ for key in ("pattern", "query", "search", "name", "path"):
57
+ if key in arguments:
58
+ value = arguments[key]
59
+ if isinstance(value, str) and len(value) < 100:
60
+ return value
61
+ return "related patterns"
@@ -0,0 +1,186 @@
1
+ """
2
+ Auto Slash Command Hook.
3
+
4
+ Detects and auto-processes slash commands in user input:
5
+ - Parses `/command` patterns in user input
6
+ - Looks up matching skill via skill_loader
7
+ - Injects skill content into prompt
8
+ - Registered as pre_model_invoke hook
9
+ """
10
+
11
+ import logging
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Pattern to match slash commands at the beginning of a line or after whitespace
19
+ SLASH_COMMAND_PATTERN = re.compile(r'(?:^|(?<=\s))\/([a-zA-Z][a-zA-Z0-9_-]*)\b', re.MULTILINE)
20
+
21
+
22
+ def extract_slash_commands(text: str) -> List[str]:
23
+ """
24
+ Extract all slash command names from text.
25
+
26
+ Args:
27
+ text: Input text to scan for slash commands
28
+
29
+ Returns:
30
+ List of command names (without the slash)
31
+ """
32
+ matches = SLASH_COMMAND_PATTERN.findall(text)
33
+ # Deduplicate while preserving order
34
+ seen = set()
35
+ unique = []
36
+ for match in matches:
37
+ if match.lower() not in seen:
38
+ seen.add(match.lower())
39
+ unique.append(match)
40
+ return unique
41
+
42
+
43
+ def load_skill_content(command_name: str, project_path: Optional[str] = None) -> Optional[Tuple[str, str]]:
44
+ """
45
+ Load skill content by command name.
46
+
47
+ Args:
48
+ command_name: The command name to look up
49
+ project_path: Optional project path for local skills
50
+
51
+ Returns:
52
+ Tuple of (skill_name, skill_content) or None if not found
53
+ """
54
+ from ..tools.skill_loader import discover_skills, parse_frontmatter
55
+
56
+ skills = discover_skills(project_path)
57
+
58
+ # Find matching skill (case-insensitive)
59
+ skill = next(
60
+ (s for s in skills if s["name"].lower() == command_name.lower()),
61
+ None
62
+ )
63
+
64
+ if not skill:
65
+ return None
66
+
67
+ try:
68
+ content = Path(skill["path"]).read_text()
69
+ metadata, body = parse_frontmatter(content)
70
+
71
+ # Build the skill injection block
72
+ skill_block = f"""
73
+ ---
74
+ ## Skill: /{skill['name']}
75
+ **Source**: {skill['path']}
76
+ """
77
+ if metadata.get("description"):
78
+ skill_block += f"**Description**: {metadata['description']}\n"
79
+
80
+ if metadata.get("allowed-tools"):
81
+ skill_block += f"**Allowed Tools**: {metadata['allowed-tools']}\n"
82
+
83
+ skill_block += f"""
84
+ ### Instructions:
85
+ {body}
86
+ ---
87
+ """
88
+ return skill["name"], skill_block
89
+
90
+ except Exception as e:
91
+ logger.error(f"[AutoSlashCommand] Error loading skill '{command_name}': {e}")
92
+ return None
93
+
94
+
95
+ def get_project_path_from_prompt(prompt: str) -> Optional[str]:
96
+ """
97
+ Try to extract project path from prompt context.
98
+ Looks for common patterns that indicate the working directory.
99
+ """
100
+ # Look for CWD markers
101
+ cwd_patterns = [
102
+ r'CWD:\s*([^\n]+)',
103
+ r'Working directory:\s*([^\n]+)',
104
+ r'project_path["\']?\s*[:=]\s*["\']?([^"\'}\n]+)',
105
+ ]
106
+
107
+ for pattern in cwd_patterns:
108
+ match = re.search(pattern, prompt)
109
+ if match:
110
+ path = match.group(1).strip()
111
+ if Path(path).exists():
112
+ return path
113
+
114
+ return None
115
+
116
+
117
+ SKILL_INJECTION_HEADER = """
118
+ > **[AUTO-SKILL INJECTION]**
119
+ > The following skill(s) have been automatically loaded based on slash commands detected in your input:
120
+ """
121
+
122
+ SKILL_NOT_FOUND_WARNING = """
123
+ > **[SKILL NOT FOUND]**
124
+ > The slash command `/{command}` was detected but no matching skill was found.
125
+ > Available skills can be listed with the `skill_list` tool.
126
+ """
127
+
128
+
129
+ async def auto_slash_command_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
130
+ """
131
+ Pre-model invoke hook that detects slash commands and injects skill content.
132
+
133
+ Scans the prompt for /command patterns and loads corresponding skill files
134
+ from .claude/commands/ directories.
135
+ """
136
+ prompt = params.get("prompt", "")
137
+
138
+ # Skip if already processed
139
+ if "[AUTO-SKILL INJECTION]" in prompt:
140
+ return None
141
+
142
+ # Extract slash commands
143
+ commands = extract_slash_commands(prompt)
144
+
145
+ if not commands:
146
+ return None
147
+
148
+ logger.info(f"[AutoSlashCommand] Detected slash commands: {commands}")
149
+
150
+ # Try to get project path from prompt context
151
+ project_path = get_project_path_from_prompt(prompt)
152
+
153
+ # Load skills for each command
154
+ injections = []
155
+ warnings = []
156
+ loaded_skills = []
157
+
158
+ for command in commands:
159
+ result = load_skill_content(command, project_path)
160
+ if result:
161
+ skill_name, skill_content = result
162
+ injections.append(skill_content)
163
+ loaded_skills.append(skill_name)
164
+ logger.info(f"[AutoSlashCommand] Loaded skill: {skill_name}")
165
+ else:
166
+ warnings.append(SKILL_NOT_FOUND_WARNING.format(command=command))
167
+ logger.warning(f"[AutoSlashCommand] Skill not found: {command}")
168
+
169
+ if not injections and not warnings:
170
+ return None
171
+
172
+ # Build the injection block
173
+ injection_block = ""
174
+
175
+ if injections:
176
+ injection_block = SKILL_INJECTION_HEADER
177
+ injection_block += f"> Skills loaded: {', '.join(loaded_skills)}\n"
178
+ injection_block += "\n".join(injections)
179
+
180
+ if warnings:
181
+ injection_block += "\n".join(warnings)
182
+
183
+ # Prepend the injection to the prompt
184
+ params["prompt"] = injection_block + "\n\n" + prompt
185
+
186
+ return params
@@ -0,0 +1,136 @@
1
+ """
2
+ Comment Checker Hook.
3
+
4
+ Warns when generated code contains excessive comments.
5
+ Code should be self-documenting; excessive comments indicate AI slop.
6
+ """
7
+
8
+ import logging
9
+ import re
10
+ from typing import Any, Dict, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ COMMENT_WARNING = """
15
+ [WARNING - EXCESSIVE COMMENTS DETECTED]
16
+
17
+ Your recent code edit contains a high ratio of comments ({ratio:.0%}).
18
+ Code generated by stravinsky should be indistinguishable from human-written code.
19
+
20
+ **Guidelines:**
21
+ - Comments should explain WHY, not WHAT
22
+ - Self-documenting code > commented code
23
+ - Remove obvious comments like "// Initialize the variable"
24
+ - Keep only: complex algorithms, security notes, performance optimizations, regex explanations
25
+
26
+ **Action Required:**
27
+ Review and remove unnecessary comments from your recent edit.
28
+ """
29
+
30
+ COMMENT_PATTERNS = {
31
+ "python": [
32
+ r"^\s*#(?!\!)",
33
+ r'^\s*"""[\s\S]*?"""',
34
+ r"^\s*'''[\s\S]*?'''",
35
+ ],
36
+ "javascript": [
37
+ r"^\s*//",
38
+ r"/\*[\s\S]*?\*/",
39
+ ],
40
+ "typescript": [
41
+ r"^\s*//",
42
+ r"/\*[\s\S]*?\*/",
43
+ ],
44
+ }
45
+
46
+ CODE_EXTENSIONS = {
47
+ ".py": "python",
48
+ ".js": "javascript",
49
+ ".ts": "typescript",
50
+ ".tsx": "typescript",
51
+ ".jsx": "javascript",
52
+ }
53
+
54
+
55
+ async def comment_checker_hook(
56
+ tool_name: str, arguments: Dict[str, Any], output: str
57
+ ) -> Optional[str]:
58
+ """
59
+ Post-tool call hook that checks for excessive comments in code edits.
60
+ """
61
+ if tool_name not in ("Write", "Edit", "write", "edit"):
62
+ return None
63
+
64
+ file_path = arguments.get("filePath", arguments.get("file_path", ""))
65
+ if not file_path:
66
+ return None
67
+
68
+ ext = None
69
+ for extension in CODE_EXTENSIONS:
70
+ if file_path.endswith(extension):
71
+ ext = extension
72
+ break
73
+
74
+ if not ext:
75
+ return None
76
+
77
+ lang = CODE_EXTENSIONS[ext]
78
+ content = arguments.get("content", arguments.get("newString", ""))
79
+
80
+ if not content or len(content) < 50:
81
+ return None
82
+
83
+ comment_ratio = _calculate_comment_ratio(content, lang)
84
+
85
+ if comment_ratio > 0.30:
86
+ logger.warning(
87
+ f"[CommentChecker] High comment ratio ({comment_ratio:.0%}) detected in {file_path}"
88
+ )
89
+ warning = COMMENT_WARNING.format(ratio=comment_ratio)
90
+ return output + "\n" + warning
91
+
92
+ return None
93
+
94
+
95
+ def _calculate_comment_ratio(content: str, lang: str) -> float:
96
+ """Calculate the ratio of comment lines to total lines."""
97
+ lines = content.split("\n")
98
+ total_lines = len([l for l in lines if l.strip()])
99
+
100
+ if total_lines == 0:
101
+ return 0.0
102
+
103
+ comment_lines = 0
104
+ patterns = COMMENT_PATTERNS.get(lang, [])
105
+
106
+ in_multiline = False
107
+ for line in lines:
108
+ stripped = line.strip()
109
+ if not stripped:
110
+ continue
111
+
112
+ if lang == "python":
113
+ if stripped.startswith("#") and not stripped.startswith("#!"):
114
+ comment_lines += 1
115
+ elif stripped.startswith('"""') or stripped.startswith("'''"):
116
+ if stripped.count('"""') >= 2 or stripped.count("'''") >= 2:
117
+ comment_lines += 1
118
+ else:
119
+ in_multiline = True
120
+ comment_lines += 1
121
+ elif in_multiline:
122
+ comment_lines += 1
123
+ if '"""' in stripped or "'''" in stripped:
124
+ in_multiline = False
125
+ else:
126
+ if stripped.startswith("//"):
127
+ comment_lines += 1
128
+ elif stripped.startswith("/*"):
129
+ in_multiline = True
130
+ comment_lines += 1
131
+ elif in_multiline:
132
+ comment_lines += 1
133
+ if "*/" in stripped:
134
+ in_multiline = False
135
+
136
+ return comment_lines / total_lines
@@ -0,0 +1,58 @@
1
+ """
2
+ Context Window Monitor Hook.
3
+
4
+ Monitors context window usage and provides headroom reminders.
5
+ At 70% usage, reminds the agent there's still capacity.
6
+ At 85%, suggests compaction before hitting hard limits.
7
+ """
8
+
9
+ import logging
10
+ from typing import Any, Dict, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ HEADROOM_REMINDER = """
15
+ [CONTEXT AWARENESS]
16
+ Current context usage: ~{usage:.0%}
17
+ You have approximately {remaining:.0%} headroom remaining.
18
+ Continue working - no compaction needed yet.
19
+ """
20
+
21
+ COMPACTION_WARNING = """
22
+ [CONTEXT WARNING - PREEMPTIVE COMPACTION RECOMMENDED]
23
+ Current context usage: ~{usage:.0%}
24
+ Approaching context limit. Consider:
25
+ 1. Completing current task quickly
26
+ 2. Using `background_cancel(all=true)` to clean up agents
27
+ 3. Summarizing findings before context overflow
28
+ """
29
+
30
+ CONTEXT_THRESHOLD_REMINDER = 0.70
31
+ CONTEXT_THRESHOLD_WARNING = 0.85
32
+ ESTIMATED_MAX_TOKENS = 200000
33
+
34
+
35
+ async def context_monitor_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
36
+ """
37
+ Pre-model invoke hook that monitors context window usage.
38
+ """
39
+ prompt = params.get("prompt", "")
40
+ estimated_tokens = len(prompt) / 4
41
+
42
+ usage_ratio = estimated_tokens / ESTIMATED_MAX_TOKENS
43
+
44
+ if usage_ratio >= CONTEXT_THRESHOLD_WARNING:
45
+ remaining = 1.0 - usage_ratio
46
+ logger.warning(f"[ContextMonitor] High context usage: {usage_ratio:.0%}")
47
+ warning = COMPACTION_WARNING.format(usage=usage_ratio, remaining=remaining)
48
+ params["prompt"] = prompt + "\n\n" + warning
49
+ return params
50
+
51
+ elif usage_ratio >= CONTEXT_THRESHOLD_REMINDER:
52
+ remaining = 1.0 - usage_ratio
53
+ logger.info(f"[ContextMonitor] Context usage: {usage_ratio:.0%}, reminding of headroom")
54
+ reminder = HEADROOM_REMINDER.format(usage=usage_ratio, remaining=remaining)
55
+ params["prompt"] = prompt + "\n\n" + reminder
56
+ return params
57
+
58
+ return None