stravinsky 0.1.12__tar.gz → 0.2.24__tar.gz
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.
- stravinsky-0.2.24/.github/workflows/publish.yml +38 -0
- stravinsky-0.2.24/.stravinsky/agents/agent_8e68511c.out +1 -0
- stravinsky-0.2.24/.stravinsky/agents/agent_9b9fd4f0.log +0 -0
- stravinsky-0.2.24/.stravinsky/agents/agent_e76617db.log +0 -0
- stravinsky-0.2.24/.stravinsky/agents/agent_e76617db.out +1 -0
- stravinsky-0.2.24/.stravinsky/agents.json +34 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/PKG-INFO +21 -9
- {stravinsky-0.1.12 → stravinsky-0.2.24}/README.md +18 -7
- stravinsky-0.2.24/error.log +4 -0
- stravinsky-0.2.24/install_native_hooks.py +53 -0
- stravinsky-0.2.24/mcp_bridge/__init__.py +1 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/auth/cli.py +7 -0
- stravinsky-0.2.24/mcp_bridge/hooks/__init__.py +28 -0
- stravinsky-0.2.24/mcp_bridge/hooks/budget_optimizer.py +38 -0
- stravinsky-0.2.24/mcp_bridge/hooks/compaction.py +32 -0
- stravinsky-0.2.24/mcp_bridge/hooks/directory_context.py +40 -0
- stravinsky-0.2.24/mcp_bridge/hooks/edit_recovery.py +41 -0
- stravinsky-0.2.24/mcp_bridge/hooks/manager.py +77 -0
- stravinsky-0.2.24/mcp_bridge/hooks/truncator.py +19 -0
- stravinsky-0.2.24/mcp_bridge/native_hooks/context.py +38 -0
- stravinsky-0.2.24/mcp_bridge/native_hooks/edit_recovery.py +46 -0
- stravinsky-0.2.24/mcp_bridge/native_hooks/truncator.py +23 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/prompts/stravinsky.py +51 -35
- stravinsky-0.2.24/mcp_bridge/server.py +519 -0
- stravinsky-0.1.12/mcp_bridge/server.py → stravinsky-0.2.24/mcp_bridge/server_tools.py +53 -372
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/__init__.py +10 -3
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/agent_manager.py +221 -107
- stravinsky-0.2.24/mcp_bridge/tools/init.py +50 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/lsp/tools.py +15 -15
- stravinsky-0.2.24/mcp_bridge/tools/model_invoke.py +561 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/task_runner.py +31 -5
- stravinsky-0.2.24/mcp_bridge/tools/templates.py +94 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/pyproject.toml +3 -2
- stravinsky-0.2.24/repro_spawn.py +29 -0
- stravinsky-0.2.24/stdout_handshake_auditor.py +85 -0
- stravinsky-0.2.24/tests/manual_test_hooks.py +57 -0
- stravinsky-0.2.24/tests/test_hooks.py +56 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/uv.lock +13 -33
- stravinsky-0.2.24/verify_tools.py +43 -0
- stravinsky-0.1.12/.stravinsky/agents.json +0 -17
- stravinsky-0.1.12/mcp_bridge/__init__.py +0 -5
- stravinsky-0.1.12/mcp_bridge/tools/model_invoke.py +0 -233
- {stravinsky-0.1.12 → stravinsky-0.2.24}/.gitignore +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/.mcp.json +0 -0
- /stravinsky-0.1.12/.stravinsky/agents/agent_9b9fd4f0.log → /stravinsky-0.2.24/.stravinsky/agents/agent_8e68511c.log +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/.stravinsky/agents/agent_9b9fd4f0.out +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/CLAUDE.md +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/assets/logo.png +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/assets/logo.png.txt +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/assets/logo_small.png +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/auth/__init__.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/auth/oauth.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/auth/openai_oauth.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/auth/token_store.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/config/__init__.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/config/hooks.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/prompts/__init__.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/prompts/delphi.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/prompts/dewey.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/prompts/document_writer.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/prompts/explore.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/prompts/frontend.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/prompts/multimodal.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/background_tasks.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/code_search.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/continuous_loop.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/lsp/__init__.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/project_context.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/session_manager.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/tools/skill_loader.py +0 -0
- {stravinsky-0.1.12 → stravinsky-0.2.24}/mcp_bridge/utils/__init__.py +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*" # Trigger on version tags like v0.2.9
|
|
7
|
+
workflow_dispatch: # Allow manual trigger
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build-and-publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # Required for trusted publishing
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.12"
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@v4
|
|
27
|
+
with:
|
|
28
|
+
version: "latest"
|
|
29
|
+
|
|
30
|
+
- name: Build package
|
|
31
|
+
run: uv build
|
|
32
|
+
|
|
33
|
+
- name: Publish to PyPI
|
|
34
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
35
|
+
with:
|
|
36
|
+
# Uses trusted publishing - no token needed if configured on PyPI
|
|
37
|
+
# Fallback to token if trusted publishing not set up:
|
|
38
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
API Error: 404 {"type":"error","error":{"type":"not_found_error","message":"model: gemini-3-flash"},"request_id":"req_011CWjjRp64NyZfRrodN8rfu"}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
API Error: 404 {"type":"error","error":{"type":"not_found_error","message":"model: gemini-3-flash"},"request_id":"req_011CWjjHp9ZHrrpZ9nNCfXAF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agent_e76617db": {
|
|
3
|
+
"id": "agent_e76617db",
|
|
4
|
+
"prompt": "Tell me a joke about robots.",
|
|
5
|
+
"agent_type": "explore",
|
|
6
|
+
"description": "Tell me a joke about robots.",
|
|
7
|
+
"status": "running",
|
|
8
|
+
"created_at": "2026-01-02T22:24:43.073103",
|
|
9
|
+
"parent_session_id": null,
|
|
10
|
+
"started_at": "2026-01-02T22:24:43.073419",
|
|
11
|
+
"completed_at": null,
|
|
12
|
+
"result": null,
|
|
13
|
+
"error": null,
|
|
14
|
+
"pid": 18206,
|
|
15
|
+
"timeout": 300,
|
|
16
|
+
"progress": null
|
|
17
|
+
},
|
|
18
|
+
"agent_8e68511c": {
|
|
19
|
+
"id": "agent_8e68511c",
|
|
20
|
+
"prompt": "Tell me a joke about robots.",
|
|
21
|
+
"agent_type": "explore",
|
|
22
|
+
"description": "Tell me a joke about robots.",
|
|
23
|
+
"status": "running",
|
|
24
|
+
"created_at": "2026-01-02T22:26:29.692048",
|
|
25
|
+
"parent_session_id": null,
|
|
26
|
+
"started_at": "2026-01-02T22:26:29.692586",
|
|
27
|
+
"completed_at": null,
|
|
28
|
+
"result": null,
|
|
29
|
+
"error": null,
|
|
30
|
+
"pid": 23792,
|
|
31
|
+
"timeout": 300,
|
|
32
|
+
"progress": null
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stravinsky
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.24
|
|
4
4
|
Summary: MCP Bridge for Claude Code with Multi-Model Support. Install globally: claude mcp add --scope user stravinsky -- uvx stravinsky. Add to CLAUDE.md: See https://pypi.org/project/stravinsky/
|
|
5
5
|
Project-URL: Repository, https://github.com/GratefulDave/stravinsky
|
|
6
6
|
Project-URL: Issues, https://github.com/GratefulDave/stravinsky/issues
|
|
@@ -15,13 +15,14 @@ Requires-Dist: google-auth>=2.20.0
|
|
|
15
15
|
Requires-Dist: httpx>=0.24.0
|
|
16
16
|
Requires-Dist: jedi>=0.19.2
|
|
17
17
|
Requires-Dist: keyring>=25.7.0
|
|
18
|
-
Requires-Dist: mcp>=1.
|
|
18
|
+
Requires-Dist: mcp>=1.2.1
|
|
19
19
|
Requires-Dist: openai>=1.0.0
|
|
20
20
|
Requires-Dist: psutil>=5.9.0
|
|
21
21
|
Requires-Dist: pydantic>=2.0.0
|
|
22
22
|
Requires-Dist: python-dotenv>=1.0.0
|
|
23
23
|
Requires-Dist: rich>=13.0.0
|
|
24
24
|
Requires-Dist: ruff>=0.14.10
|
|
25
|
+
Requires-Dist: tenacity>=8.5.0
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
27
28
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
@@ -76,7 +77,7 @@ claude mcp add stravinsky -- stravinsky
|
|
|
76
77
|
|
|
77
78
|
### Authentication
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
````bash
|
|
80
81
|
# Login to Google (Gemini)
|
|
81
82
|
stravinsky-auth login gemini
|
|
82
83
|
|
|
@@ -88,7 +89,22 @@ stravinsky-auth status
|
|
|
88
89
|
|
|
89
90
|
# Logout
|
|
90
91
|
stravinsky-auth logout gemini
|
|
91
|
-
|
|
92
|
+
|
|
93
|
+
### Repo Auto-Initialization
|
|
94
|
+
|
|
95
|
+
Bootstrap any repository for Stravinsky in one command:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# In the root of your project:
|
|
99
|
+
stravinsky-auth init
|
|
100
|
+
````
|
|
101
|
+
|
|
102
|
+
This will:
|
|
103
|
+
|
|
104
|
+
1. Create/Update `CLAUDE.md` with Stravinsky parallel execution rules.
|
|
105
|
+
2. Install standard slash commands into `.claude/commands/stra/`.
|
|
106
|
+
|
|
107
|
+
````
|
|
92
108
|
|
|
93
109
|
## Add to Your CLAUDE.md
|
|
94
110
|
|
|
@@ -126,7 +142,7 @@ For ANY task with 2+ independent steps:
|
|
|
126
142
|
|
|
127
143
|
- **ULTRATHINK**: Engage exhaustive deep reasoning, multi-dimensional analysis
|
|
128
144
|
- **ULTRAWORK**: Maximum parallel execution - spawn agents aggressively for every subtask
|
|
129
|
-
|
|
145
|
+
````
|
|
130
146
|
|
|
131
147
|
## Tools (31)
|
|
132
148
|
|
|
@@ -192,7 +208,3 @@ The Codex CLI uses the same port. Stop it with: `killall codex`
|
|
|
192
208
|
MIT
|
|
193
209
|
|
|
194
210
|
---
|
|
195
|
-
|
|
196
|
-
<div align="center">
|
|
197
|
-
<small>Built with obsession by the Google Deepmind team.</small>
|
|
198
|
-
</div>
|
|
@@ -45,7 +45,7 @@ claude mcp add stravinsky -- stravinsky
|
|
|
45
45
|
|
|
46
46
|
### Authentication
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
````bash
|
|
49
49
|
# Login to Google (Gemini)
|
|
50
50
|
stravinsky-auth login gemini
|
|
51
51
|
|
|
@@ -57,7 +57,22 @@ stravinsky-auth status
|
|
|
57
57
|
|
|
58
58
|
# Logout
|
|
59
59
|
stravinsky-auth logout gemini
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
### Repo Auto-Initialization
|
|
62
|
+
|
|
63
|
+
Bootstrap any repository for Stravinsky in one command:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# In the root of your project:
|
|
67
|
+
stravinsky-auth init
|
|
68
|
+
````
|
|
69
|
+
|
|
70
|
+
This will:
|
|
71
|
+
|
|
72
|
+
1. Create/Update `CLAUDE.md` with Stravinsky parallel execution rules.
|
|
73
|
+
2. Install standard slash commands into `.claude/commands/stra/`.
|
|
74
|
+
|
|
75
|
+
````
|
|
61
76
|
|
|
62
77
|
## Add to Your CLAUDE.md
|
|
63
78
|
|
|
@@ -95,7 +110,7 @@ For ANY task with 2+ independent steps:
|
|
|
95
110
|
|
|
96
111
|
- **ULTRATHINK**: Engage exhaustive deep reasoning, multi-dimensional analysis
|
|
97
112
|
- **ULTRAWORK**: Maximum parallel execution - spawn agents aggressively for every subtask
|
|
98
|
-
|
|
113
|
+
````
|
|
99
114
|
|
|
100
115
|
## Tools (31)
|
|
101
116
|
|
|
@@ -161,7 +176,3 @@ The Codex CLI uses the same port. Stop it with: `killall codex`
|
|
|
161
176
|
MIT
|
|
162
177
|
|
|
163
178
|
---
|
|
164
|
-
|
|
165
|
-
<div align="center">
|
|
166
|
-
<small>Built with obsession by the Google Deepmind team.</small>
|
|
167
|
-
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def install():
|
|
6
|
+
project_root = Path.cwd()
|
|
7
|
+
settings_path = project_root / ".claude" / "settings.json"
|
|
8
|
+
|
|
9
|
+
# Create .claude directory if missing
|
|
10
|
+
settings_path.parent.mkdir(exist_ok=True)
|
|
11
|
+
|
|
12
|
+
# Initial settings
|
|
13
|
+
if settings_path.exists():
|
|
14
|
+
try:
|
|
15
|
+
settings = json.loads(settings_path.read_text())
|
|
16
|
+
except Exception:
|
|
17
|
+
settings = {}
|
|
18
|
+
else:
|
|
19
|
+
settings = {}
|
|
20
|
+
|
|
21
|
+
if "hooks" not in settings:
|
|
22
|
+
settings["hooks"] = {}
|
|
23
|
+
|
|
24
|
+
hooks = settings["hooks"]
|
|
25
|
+
|
|
26
|
+
# Hook 1: UserPromptSubmit (Context)
|
|
27
|
+
hooks["UserPromptSubmit"] = [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": f"python3 {project_root}/mcp_bridge/native_hooks/context.py"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Hook 2: PostToolUse (Recovery & Truncation)
|
|
35
|
+
# We chain them or use a dispatcher. Chaining is easier in the JSON.
|
|
36
|
+
hooks["PostToolUse"] = [
|
|
37
|
+
{
|
|
38
|
+
"type": "command",
|
|
39
|
+
"command": f"python3 {project_root}/mcp_bridge/native_hooks/truncator.py"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": f"python3 {project_root}/mcp_bridge/native_hooks/edit_recovery.py",
|
|
44
|
+
"matcher": ["Edit", "MultiEdit"]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Save
|
|
49
|
+
settings_path.write_text(json.dumps(settings, indent=2))
|
|
50
|
+
print(f"✅ Stravinsky native hooks installed to {settings_path}")
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
install()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.24"
|
|
@@ -9,6 +9,7 @@ import sys
|
|
|
9
9
|
import time
|
|
10
10
|
|
|
11
11
|
from .token_store import TokenStore
|
|
12
|
+
from ..tools.init import bootstrap_repo
|
|
12
13
|
from .oauth import perform_oauth_flow as gemini_oauth, refresh_access_token as gemini_refresh
|
|
13
14
|
from .openai_oauth import (
|
|
14
15
|
perform_oauth_flow as openai_oauth,
|
|
@@ -184,6 +185,9 @@ def main():
|
|
|
184
185
|
help="Provider to refresh token for",
|
|
185
186
|
)
|
|
186
187
|
|
|
188
|
+
# init command
|
|
189
|
+
subparsers.add_parser("init", help="Bootstrap current repository for Stravinsky")
|
|
190
|
+
|
|
187
191
|
args = parser.parse_args()
|
|
188
192
|
|
|
189
193
|
if not args.command:
|
|
@@ -200,6 +204,9 @@ def main():
|
|
|
200
204
|
return cmd_status(token_store)
|
|
201
205
|
elif args.command == "refresh":
|
|
202
206
|
return cmd_refresh(args.provider, token_store)
|
|
207
|
+
elif args.command == "init":
|
|
208
|
+
print(bootstrap_repo())
|
|
209
|
+
return 0
|
|
203
210
|
|
|
204
211
|
return 0
|
|
205
212
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hooks initialization.
|
|
3
|
+
Registers all Tier 1-3 hooks into the HookManager.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .manager import get_hook_manager
|
|
7
|
+
from .truncator import output_truncator_hook
|
|
8
|
+
from .edit_recovery import edit_error_recovery_hook
|
|
9
|
+
from .directory_context import directory_context_hook
|
|
10
|
+
from .compaction import context_compaction_hook
|
|
11
|
+
from .budget_optimizer import budget_optimizer_hook
|
|
12
|
+
|
|
13
|
+
def initialize_hooks():
|
|
14
|
+
"""Register all available hooks."""
|
|
15
|
+
manager = get_hook_manager()
|
|
16
|
+
|
|
17
|
+
# Tier 1
|
|
18
|
+
manager.register_post_tool_call(output_truncator_hook)
|
|
19
|
+
manager.register_post_tool_call(edit_error_recovery_hook)
|
|
20
|
+
|
|
21
|
+
# Tier 2
|
|
22
|
+
manager.register_pre_model_invoke(directory_context_hook)
|
|
23
|
+
manager.register_pre_model_invoke(context_compaction_hook)
|
|
24
|
+
|
|
25
|
+
# Tier 3
|
|
26
|
+
manager.register_pre_model_invoke(budget_optimizer_hook)
|
|
27
|
+
|
|
28
|
+
# initialize_hooks()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thinking budget optimizer hook.
|
|
3
|
+
Analyzes prompt complexity and adjusts thinking_budget for models that support it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
REASONING_KEYWORDS = [
|
|
9
|
+
"architect", "design", "refactor", "debug", "complex", "optimize",
|
|
10
|
+
"summarize", "analyze", "explain", "why", "review", "strangler"
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
async def budget_optimizer_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
14
|
+
"""
|
|
15
|
+
Adjusts the thinking_budget based on presence of reasoning-heavy keywords.
|
|
16
|
+
"""
|
|
17
|
+
model = params.get("model", "")
|
|
18
|
+
# Only applies to models that typically support reasoning budgets (Gemini 2.0 Thinking, GPT-o1, etc.)
|
|
19
|
+
if not any(m in model for m in ["thinking", "flash-thinking", "o1", "o3"]):
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
prompt = params.get("prompt", "").lower()
|
|
23
|
+
|
|
24
|
+
# Simple heuristic
|
|
25
|
+
is_complex = any(keyword in prompt for keyword in REASONING_KEYWORDS)
|
|
26
|
+
|
|
27
|
+
current_budget = params.get("thinking_budget", 0)
|
|
28
|
+
|
|
29
|
+
if is_complex and current_budget < 4000:
|
|
30
|
+
# Increase budget for complex tasks
|
|
31
|
+
params["thinking_budget"] = 16000
|
|
32
|
+
return params
|
|
33
|
+
elif not is_complex and current_budget > 2000:
|
|
34
|
+
# Lower budget for simple tasks to save time/cost
|
|
35
|
+
params["thinking_budget"] = 2000
|
|
36
|
+
return params
|
|
37
|
+
|
|
38
|
+
return None
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Preemptive context compaction hook.
|
|
3
|
+
Monitors context size and injects optimization reminders.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
THRESHOLD_CHARS = 100000 # Roughly 25k-30k tokens for typical LLM text
|
|
9
|
+
|
|
10
|
+
COMPACTION_REMINDER = """
|
|
11
|
+
> **[SYSTEM ALERT - CONTEXT WINDOW NEAR LIMIT]**
|
|
12
|
+
> The current conversation history is reaching its limits. Performance may degrade.
|
|
13
|
+
> Please **STOP** and perform a **Session Compaction**:
|
|
14
|
+
> 1. Summarize all work completed so far in a `TASK_STATE.md` (if not already done).
|
|
15
|
+
> 2. List all pending todos.
|
|
16
|
+
> 3. Clear unnecessary tool outputs from your reasoning.
|
|
17
|
+
> 4. Keep your next responses concise and focused only on the current sub-task.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
async def context_compaction_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
21
|
+
"""
|
|
22
|
+
Checks prompt length and injects a compaction reminder if it's too large.
|
|
23
|
+
"""
|
|
24
|
+
prompt = params.get("prompt", "")
|
|
25
|
+
|
|
26
|
+
if len(prompt) > THRESHOLD_CHARS:
|
|
27
|
+
# Check if we haven't already injected the reminder recently
|
|
28
|
+
if "CONTEXT WINDOW NEAR LIMIT" not in prompt:
|
|
29
|
+
params["prompt"] = COMPACTION_REMINDER + prompt
|
|
30
|
+
return params
|
|
31
|
+
|
|
32
|
+
return None
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Directory context injector hook.
|
|
3
|
+
Automatically finds and injects local AGENTS.md or README.md content based on the current context.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
async def directory_context_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
11
|
+
"""
|
|
12
|
+
Search for AGENTS.md or README.md in the current working directory and inject them.
|
|
13
|
+
"""
|
|
14
|
+
cwd = Path.cwd()
|
|
15
|
+
|
|
16
|
+
# Check for AGENTS.md or README.md
|
|
17
|
+
target_files = ["AGENTS.md", "README.md"]
|
|
18
|
+
found_file = None
|
|
19
|
+
for filename in target_files:
|
|
20
|
+
if (cwd / filename).exists():
|
|
21
|
+
found_file = cwd / filename
|
|
22
|
+
break
|
|
23
|
+
|
|
24
|
+
if not found_file:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
content = found_file.read_text()
|
|
29
|
+
# Injects as a special system reminder
|
|
30
|
+
injection = f"\n\n### Local Directory Context ({found_file.name}):\n{content}\n"
|
|
31
|
+
|
|
32
|
+
# Modify the prompt if it exists in params
|
|
33
|
+
if "prompt" in params:
|
|
34
|
+
# Add to the beginning of the prompt as a context block
|
|
35
|
+
params["prompt"] = injection + params["prompt"]
|
|
36
|
+
return params
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
return None
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Edit error recovery hook.
|
|
3
|
+
Detects common mistakes in file editing and injects high-priority corrective directives.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
EDIT_ERROR_PATTERNS = [
|
|
10
|
+
r"oldString and newString must be different",
|
|
11
|
+
r"oldString not found",
|
|
12
|
+
r"oldString found multiple times",
|
|
13
|
+
r"Target content not found",
|
|
14
|
+
r"Multiple occurrences of target content found",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
EDIT_RECOVERY_PROMPT = """
|
|
18
|
+
> **[EDIT ERROR - IMMEDIATE ACTION REQUIRED]**
|
|
19
|
+
> You made an Edit mistake. STOP and do this NOW:
|
|
20
|
+
> 1. **READ** the file immediately to see its ACTUAL current state.
|
|
21
|
+
> 2. **VERIFY** what the content really looks like (your assumption was wrong).
|
|
22
|
+
> 3. **APOLOGIZE** briefly to the user for the error.
|
|
23
|
+
> 4. **CONTINUE** with corrected action based on the real file content.
|
|
24
|
+
> **DO NOT** attempt another edit until you've read and verified the file state.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
async def edit_error_recovery_hook(tool_name: str, arguments: Dict[str, Any], output: str) -> Optional[str]:
|
|
28
|
+
"""
|
|
29
|
+
Analyzes tool output for edit errors and appends corrective directives.
|
|
30
|
+
"""
|
|
31
|
+
# Check if this is an edit-related tool (handling both built-in and common MCP tools)
|
|
32
|
+
edit_tools = ["replace_file_content", "multi_replace_file_content", "write_to_file", "edit_file", "Edit"]
|
|
33
|
+
|
|
34
|
+
# We also check the output content for common patterns even if the tool name doesn't match perfectly
|
|
35
|
+
is_edit_error = any(re.search(pattern, output, re.IGNORECASE) for pattern in EDIT_ERROR_PATTERNS)
|
|
36
|
+
|
|
37
|
+
if is_edit_error or any(tool in tool_name for tool in edit_tools):
|
|
38
|
+
if any(re.search(pattern, output, re.IGNORECASE) for pattern in EDIT_ERROR_PATTERNS):
|
|
39
|
+
return output + EDIT_RECOVERY_PROMPT
|
|
40
|
+
|
|
41
|
+
return None
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modular Hook System for Stravinsky.
|
|
3
|
+
Provides interception points for tool calls and model invocations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Awaitable
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
class HookManager:
|
|
12
|
+
"""
|
|
13
|
+
Manages the registration and execution of hooks.
|
|
14
|
+
"""
|
|
15
|
+
_instance = None
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.pre_tool_call_hooks: List[Callable[[str, Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]] = []
|
|
19
|
+
self.post_tool_call_hooks: List[Callable[[str, Dict[str, Any], str], Awaitable[Optional[str]]]] = []
|
|
20
|
+
self.pre_model_invoke_hooks: List[Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]] = []
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def get_instance(cls):
|
|
24
|
+
if cls._instance is None:
|
|
25
|
+
cls._instance = cls()
|
|
26
|
+
return cls._instance
|
|
27
|
+
|
|
28
|
+
def register_pre_tool_call(self, hook: Callable[[str, Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]):
|
|
29
|
+
"""Run before a tool is called. Can modify arguments or return early result."""
|
|
30
|
+
self.pre_tool_call_hooks.append(hook)
|
|
31
|
+
|
|
32
|
+
def register_post_tool_call(self, hook: Callable[[str, Dict[str, Any], str], Awaitable[Optional[str]]]):
|
|
33
|
+
"""Run after a tool call. Can modify or recover from tool output/error."""
|
|
34
|
+
self.post_tool_call_hooks.append(hook)
|
|
35
|
+
|
|
36
|
+
def register_pre_model_invoke(self, hook: Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]):
|
|
37
|
+
"""Run before model invocation. Can modify prompt or parameters."""
|
|
38
|
+
self.pre_model_invoke_hooks.append(hook)
|
|
39
|
+
|
|
40
|
+
async def execute_pre_tool_call(self, tool_name: str, arguments: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
41
|
+
"""Executes all pre-tool call hooks."""
|
|
42
|
+
current_args = arguments
|
|
43
|
+
for hook in self.pre_tool_call_hooks:
|
|
44
|
+
try:
|
|
45
|
+
modified_args = await hook(tool_name, current_args)
|
|
46
|
+
if modified_args is not None:
|
|
47
|
+
current_args = modified_args
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f"[HookManager] Error in pre_tool_call hook {hook.__name__}: {e}")
|
|
50
|
+
return current_args
|
|
51
|
+
|
|
52
|
+
async def execute_post_tool_call(self, tool_name: str, arguments: Dict[str, Any], output: str) -> str:
|
|
53
|
+
"""Executes all post-tool call hooks."""
|
|
54
|
+
current_output = output
|
|
55
|
+
for hook in self.post_tool_call_hooks:
|
|
56
|
+
try:
|
|
57
|
+
modified_output = await hook(tool_name, arguments, current_output)
|
|
58
|
+
if modified_output is not None:
|
|
59
|
+
current_output = modified_output
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"[HookManager] Error in post_tool_call hook {hook.__name__}: {e}")
|
|
62
|
+
return current_output
|
|
63
|
+
|
|
64
|
+
async def execute_pre_model_invoke(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
65
|
+
"""Executes all pre-model invoke hooks."""
|
|
66
|
+
current_params = params
|
|
67
|
+
for hook in self.pre_model_invoke_hooks:
|
|
68
|
+
try:
|
|
69
|
+
modified_params = await hook(current_params)
|
|
70
|
+
if modified_params is not None:
|
|
71
|
+
current_params = modified_params
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"[HookManager] Error in pre_model_invoke hook {hook.__name__}: {e}")
|
|
74
|
+
return current_params
|
|
75
|
+
|
|
76
|
+
def get_hook_manager() -> HookManager:
|
|
77
|
+
return HookManager.get_instance()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool output truncator hook.
|
|
3
|
+
Limits the size of tool outputs to prevent context bloat.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
async def output_truncator_hook(tool_name: str, arguments: Dict[str, Any], output: str) -> Optional[str]:
|
|
9
|
+
"""
|
|
10
|
+
Truncates tool output if it exceeds a certain length.
|
|
11
|
+
"""
|
|
12
|
+
MAX_LENGTH = 30000 # 30k characters limit
|
|
13
|
+
|
|
14
|
+
if len(output) > MAX_LENGTH:
|
|
15
|
+
truncated = output[:MAX_LENGTH]
|
|
16
|
+
summary = f"\n\n... (Result truncated from {len(output)} chars to {MAX_LENGTH} chars) ..."
|
|
17
|
+
return truncated + summary
|
|
18
|
+
|
|
19
|
+
return None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
try:
|
|
8
|
+
data = json.load(sys.stdin)
|
|
9
|
+
prompt = data.get("prompt", "")
|
|
10
|
+
except Exception:
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
cwd = Path(os.environ.get("CLAUDE_CWD", "."))
|
|
14
|
+
|
|
15
|
+
# Files to look for
|
|
16
|
+
context_files = ["AGENTS.md", "README.md", "CLAUDE.md"]
|
|
17
|
+
found_context = ""
|
|
18
|
+
|
|
19
|
+
for f in context_files:
|
|
20
|
+
path = cwd / f
|
|
21
|
+
if path.exists():
|
|
22
|
+
try:
|
|
23
|
+
content = path.read_text()
|
|
24
|
+
found_context += f"\n\n--- LOCAL CONTEXT: {f} ---\n{content}\n"
|
|
25
|
+
break # Only use one for brevity
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
if found_context:
|
|
30
|
+
# Prepend context to prompt
|
|
31
|
+
# We wrap the user prompt to distinguish it
|
|
32
|
+
new_prompt = f"{found_context}\n\n[USER PROMPT]\n{prompt}"
|
|
33
|
+
print(new_prompt)
|
|
34
|
+
else:
|
|
35
|
+
print(prompt)
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
# Claude Code PostToolUse inputs via Environment Variables
|
|
8
|
+
tool_name = os.environ.get("CLAUDE_TOOL_NAME")
|
|
9
|
+
|
|
10
|
+
# We only care about Edit/MultiEdit
|
|
11
|
+
if tool_name not in ["Edit", "MultiEdit"]:
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
# Read from stdin (Claude Code passes the tool response via stdin for some hook types,
|
|
15
|
+
# but for PostToolUse it's often better to check the environment variable if available.
|
|
16
|
+
# Actually, the summary says input is a JSON payload.
|
|
17
|
+
try:
|
|
18
|
+
data = json.load(sys.stdin)
|
|
19
|
+
tool_response = data.get("tool_response", "")
|
|
20
|
+
except Exception:
|
|
21
|
+
# Fallback to direct string if not JSON
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
# Error patterns
|
|
25
|
+
error_patterns = [
|
|
26
|
+
r"oldString not found",
|
|
27
|
+
r"oldString matched multiple times",
|
|
28
|
+
r"line numbers out of range"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
recovery_needed = any(re.search(p, tool_response, re.IGNORECASE) for p in error_patterns)
|
|
32
|
+
|
|
33
|
+
if recovery_needed:
|
|
34
|
+
correction = (
|
|
35
|
+
"\n\n[SYSTEM RECOVERY] It appears the Edit tool failed to find the target string. "
|
|
36
|
+
"Please call 'Read' on the file again to verify the current content, "
|
|
37
|
+
"then ensure your 'oldString' is an EXACT match including all whitespace."
|
|
38
|
+
)
|
|
39
|
+
# For PostToolUse, stdout is captured and appended/replaces output
|
|
40
|
+
print(tool_response + correction)
|
|
41
|
+
else:
|
|
42
|
+
# No change
|
|
43
|
+
print(tool_response)
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|