stravinsky 0.2.67__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 +112 -11
- 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/MANIFEST_SCHEMA.md +305 -0
- mcp_bridge/config/README.md +276 -0
- mcp_bridge/config/__init__.py +2 -2
- mcp_bridge/config/hook_config.py +247 -0
- mcp_bridge/config/hooks_manifest.json +138 -0
- mcp_bridge/config/rate_limits.py +317 -0
- mcp_bridge/config/skills_manifest.json +128 -0
- mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
- mcp_bridge/hooks/__init__.py +19 -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 +43 -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/tool_messaging.py +113 -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 +150 -0
- 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 +542 -59
- mcp_bridge/server_tools.py +738 -6
- mcp_bridge/tools/__init__.py +40 -25
- mcp_bridge/tools/agent_manager.py +616 -697
- mcp_bridge/tools/background_tasks.py +13 -17
- mcp_bridge/tools/code_search.py +70 -53
- 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 +12 -5
- mcp_bridge/tools/lsp/manager.py +471 -0
- mcp_bridge/tools/lsp/tools.py +723 -207
- mcp_bridge/tools/model_invoke.py +1195 -273
- mcp_bridge/tools/mux_client.py +75 -0
- mcp_bridge/tools/project_context.py +1 -2
- mcp_bridge/tools/query_classifier.py +406 -0
- 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 +3627 -0
- 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 +585 -0
- mcp_bridge/update_manager_pypi.py +297 -0
- 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.66.dist-info/METADATA +517 -0
- stravinsky-0.4.66.dist-info/RECORD +198 -0
- {stravinsky-0.2.67.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.2.67.dist-info/METADATA +0 -284
- stravinsky-0.2.67.dist-info/RECORD +0 -76
- {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
mcp_bridge/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.4.66"
|
mcp_bridge/auth/__init__.py
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
# Authentication module
|
|
2
|
-
from .token_store import TokenStore, TokenData
|
|
3
2
|
from .oauth import (
|
|
4
|
-
perform_oauth_flow as gemini_oauth_flow,
|
|
5
|
-
refresh_access_token as gemini_refresh_token,
|
|
6
3
|
ANTIGRAVITY_CLIENT_ID,
|
|
7
|
-
ANTIGRAVITY_SCOPES,
|
|
8
4
|
ANTIGRAVITY_HEADERS,
|
|
5
|
+
ANTIGRAVITY_SCOPES,
|
|
6
|
+
)
|
|
7
|
+
from .oauth import (
|
|
8
|
+
perform_oauth_flow as gemini_oauth_flow,
|
|
9
|
+
)
|
|
10
|
+
from .oauth import (
|
|
11
|
+
refresh_access_token as gemini_refresh_token,
|
|
9
12
|
)
|
|
10
13
|
from .openai_oauth import (
|
|
11
|
-
perform_oauth_flow as openai_oauth_flow,
|
|
12
|
-
refresh_access_token as openai_refresh_token,
|
|
13
14
|
CLIENT_ID as OPENAI_CLIENT_ID,
|
|
15
|
+
)
|
|
16
|
+
from .openai_oauth import (
|
|
14
17
|
OPENAI_CALLBACK_PORT,
|
|
15
18
|
)
|
|
19
|
+
from .openai_oauth import (
|
|
20
|
+
perform_oauth_flow as openai_oauth_flow,
|
|
21
|
+
)
|
|
22
|
+
from .openai_oauth import (
|
|
23
|
+
refresh_access_token as openai_refresh_token,
|
|
24
|
+
)
|
|
25
|
+
from .token_store import TokenData, TokenStore
|
|
16
26
|
|
|
17
27
|
__all__ = [
|
|
18
28
|
# Token Store
|
mcp_bridge/auth/cli.py
CHANGED
|
@@ -14,16 +14,26 @@ Usage:
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import argparse
|
|
17
|
+
import json
|
|
17
18
|
import sys
|
|
18
19
|
import time
|
|
20
|
+
from pathlib import Path
|
|
19
21
|
|
|
20
|
-
from .token_store import TokenStore
|
|
21
22
|
from ..tools.init import bootstrap_repo
|
|
22
|
-
from .oauth import perform_oauth_flow as gemini_oauth
|
|
23
|
+
from .oauth import perform_oauth_flow as gemini_oauth
|
|
24
|
+
from .oauth import refresh_access_token as gemini_refresh
|
|
23
25
|
from .openai_oauth import (
|
|
24
26
|
perform_oauth_flow as openai_oauth,
|
|
27
|
+
)
|
|
28
|
+
from .openai_oauth import (
|
|
25
29
|
refresh_access_token as openai_refresh,
|
|
26
30
|
)
|
|
31
|
+
from .token_store import TokenStore
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from ..routing import get_provider_tracker
|
|
35
|
+
except ImportError:
|
|
36
|
+
get_provider_tracker = None # type: ignore
|
|
27
37
|
|
|
28
38
|
|
|
29
39
|
def cmd_login(provider: str, token_store: TokenStore) -> int:
|
|
@@ -34,7 +44,7 @@ def cmd_login(provider: str, token_store: TokenStore) -> int:
|
|
|
34
44
|
For OpenAI: Uses OpenAI OAuth (ChatGPT Plus/Pro subscription)
|
|
35
45
|
"""
|
|
36
46
|
if provider == "gemini":
|
|
37
|
-
print(
|
|
47
|
+
print("Starting Google OAuth for Gemini...")
|
|
38
48
|
|
|
39
49
|
try:
|
|
40
50
|
result = gemini_oauth()
|
|
@@ -57,7 +67,7 @@ def cmd_login(provider: str, token_store: TokenStore) -> int:
|
|
|
57
67
|
return 1
|
|
58
68
|
|
|
59
69
|
elif provider == "openai":
|
|
60
|
-
print(
|
|
70
|
+
print("Starting OpenAI OAuth for ChatGPT Plus/Pro...")
|
|
61
71
|
print("Note: Requires ChatGPT Plus/Pro subscription and port 1455 available")
|
|
62
72
|
|
|
63
73
|
try:
|
|
@@ -72,7 +82,7 @@ def cmd_login(provider: str, token_store: TokenStore) -> int:
|
|
|
72
82
|
expires_at=expires_at,
|
|
73
83
|
)
|
|
74
84
|
|
|
75
|
-
print(
|
|
85
|
+
print("\n✓ Successfully authenticated with OpenAI")
|
|
76
86
|
print(f" Token expires in: {result.expires_in // 60} minutes")
|
|
77
87
|
return 0
|
|
78
88
|
|
|
@@ -116,20 +126,156 @@ def cmd_status(token_store: TokenStore) -> int:
|
|
|
116
126
|
minutes = (remaining % 3600) // 60
|
|
117
127
|
print(f" Expires in: {hours}h {minutes}m")
|
|
118
128
|
else:
|
|
119
|
-
print(
|
|
129
|
+
print(" Token expired")
|
|
120
130
|
|
|
121
131
|
print()
|
|
122
132
|
return 0
|
|
123
133
|
|
|
124
134
|
|
|
125
135
|
def cmd_refresh(provider: str, token_store: TokenStore) -> int:
|
|
126
|
-
"""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
"""Manually refresh an access token."""
|
|
137
|
+
try:
|
|
138
|
+
token = token_store.get_token(provider)
|
|
139
|
+
if not token:
|
|
140
|
+
print(f"Not logged in to {provider}. Run 'stravinsky-auth login {provider}' first.")
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
print(f"Refreshing {provider} access token...")
|
|
144
|
+
|
|
145
|
+
if provider == "gemini":
|
|
146
|
+
result = gemini_refresh(token["refresh_token"])
|
|
147
|
+
elif provider == "openai":
|
|
148
|
+
result = openai_refresh(token["refresh_token"])
|
|
149
|
+
else:
|
|
150
|
+
print(f"Refresh not supported for {provider}")
|
|
151
|
+
return 1
|
|
152
|
+
|
|
153
|
+
expires_at = int(time.time()) + result.expires_in
|
|
154
|
+
|
|
155
|
+
token_store.set_token(
|
|
156
|
+
provider=provider,
|
|
157
|
+
access_token=result.access_token,
|
|
158
|
+
refresh_token=result.refresh_token or token["refresh_token"],
|
|
159
|
+
expires_at=expires_at,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
print(f"✓ Token refreshed, expires in {result.expires_in // 60} minutes")
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
print(f"✗ Refresh failed: {e}", file=sys.stderr)
|
|
167
|
+
return 1
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def cmd_routing_status() -> int:
|
|
171
|
+
"""Show current routing provider states."""
|
|
172
|
+
if not get_provider_tracker or not callable(get_provider_tracker):
|
|
173
|
+
print("✗ Routing module not available", file=sys.stderr)
|
|
131
174
|
return 1
|
|
132
175
|
|
|
176
|
+
try:
|
|
177
|
+
tracker = get_provider_tracker()
|
|
178
|
+
status = tracker.get_status()
|
|
179
|
+
|
|
180
|
+
print("\n📊 Provider Routing Status\n")
|
|
181
|
+
for provider_name, state in status.items():
|
|
182
|
+
available = "✓ Available" if state["available"] else "⏳ In Cooldown"
|
|
183
|
+
print(f"{provider_name.title():10} {available}")
|
|
184
|
+
|
|
185
|
+
if state["cooldown_remaining"]:
|
|
186
|
+
mins = int(state["cooldown_remaining"] / 60)
|
|
187
|
+
secs = int(state["cooldown_remaining"] % 60)
|
|
188
|
+
print(f" Cooldown: {mins}m {secs}s remaining")
|
|
189
|
+
|
|
190
|
+
print(f" Requests: {state['total_requests']}")
|
|
191
|
+
print(f" Failures: {state['total_failures']}")
|
|
192
|
+
|
|
193
|
+
if state["last_error"]:
|
|
194
|
+
print(f" Last error: {state['last_error']}")
|
|
195
|
+
|
|
196
|
+
print()
|
|
197
|
+
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
print(f"✗ Failed to get routing status: {e}", file=sys.stderr)
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def cmd_routing_reset(provider: str | None = None) -> int:
|
|
206
|
+
"""Reset provider cooldown states."""
|
|
207
|
+
if not get_provider_tracker or not callable(get_provider_tracker):
|
|
208
|
+
print("✗ Routing module not available", file=sys.stderr)
|
|
209
|
+
return 1
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
tracker = get_provider_tracker()
|
|
213
|
+
|
|
214
|
+
if provider:
|
|
215
|
+
tracker.reset_provider(provider)
|
|
216
|
+
print(f"✓ Reset cooldown state for {provider}")
|
|
217
|
+
else:
|
|
218
|
+
tracker.reset_all()
|
|
219
|
+
print("✓ Reset all provider cooldown states")
|
|
220
|
+
|
|
221
|
+
return 0
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f"✗ Failed to reset routing state: {e}", file=sys.stderr)
|
|
225
|
+
return 1
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def cmd_routing_init() -> int:
|
|
229
|
+
"""Create default routing.json configuration."""
|
|
230
|
+
config_path = Path(".stravinsky/routing.json")
|
|
231
|
+
|
|
232
|
+
if config_path.exists():
|
|
233
|
+
response = input(f"{config_path} already exists. Overwrite? [y/N]: ")
|
|
234
|
+
if response.lower() != "y":
|
|
235
|
+
print("Cancelled.")
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
|
|
240
|
+
default_config = {
|
|
241
|
+
"routing": {
|
|
242
|
+
"task_routing": {
|
|
243
|
+
"code_generation": {
|
|
244
|
+
"provider": "openai",
|
|
245
|
+
"model": "gpt-5-codex",
|
|
246
|
+
"description": "Complex code generation tasks",
|
|
247
|
+
},
|
|
248
|
+
"debugging": {
|
|
249
|
+
"provider": "openai",
|
|
250
|
+
"model": "gpt-5-codex",
|
|
251
|
+
"description": "Code analysis and debugging",
|
|
252
|
+
},
|
|
253
|
+
"documentation": {
|
|
254
|
+
"provider": "gemini",
|
|
255
|
+
"model": "gemini-3-flash",
|
|
256
|
+
"description": "Documentation writing",
|
|
257
|
+
},
|
|
258
|
+
"code_search": {
|
|
259
|
+
"provider": "gemini",
|
|
260
|
+
"model": "gemini-3-flash",
|
|
261
|
+
"description": "Finding code patterns",
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
"fallback": {
|
|
265
|
+
"enabled": True,
|
|
266
|
+
"chain": ["claude", "openai", "gemini"],
|
|
267
|
+
"cooldown_seconds": 300,
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
with open(config_path, "w") as f:
|
|
273
|
+
json.dump(default_config, f, indent=2)
|
|
274
|
+
|
|
275
|
+
print(f"✓ Created {config_path}")
|
|
276
|
+
print("\nEdit this file to customize routing behavior for this project.")
|
|
277
|
+
return 0
|
|
278
|
+
|
|
133
279
|
try:
|
|
134
280
|
print(f"Refreshing {provider} token...")
|
|
135
281
|
|
|
@@ -226,6 +372,41 @@ def main():
|
|
|
226
372
|
description="Creates .stravinsky/ directory structure and copies default configuration files.",
|
|
227
373
|
)
|
|
228
374
|
|
|
375
|
+
# routing command
|
|
376
|
+
routing_parser = subparsers.add_parser(
|
|
377
|
+
"routing",
|
|
378
|
+
help="Manage provider routing and fallback",
|
|
379
|
+
description="View and manage provider routing states, cooldowns, and configuration.",
|
|
380
|
+
)
|
|
381
|
+
routing_subparsers = routing_parser.add_subparsers(
|
|
382
|
+
dest="routing_command", help="Routing subcommands", metavar="SUBCOMMAND"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
routing_subparsers.add_parser(
|
|
386
|
+
"status",
|
|
387
|
+
help="Show current provider routing states",
|
|
388
|
+
description="Display availability, cooldown timers, and error counts for all providers.",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
routing_reset_parser = routing_subparsers.add_parser(
|
|
392
|
+
"reset",
|
|
393
|
+
help="Reset provider cooldown states",
|
|
394
|
+
description="Clear cooldown timers for one or all providers.",
|
|
395
|
+
)
|
|
396
|
+
routing_reset_parser.add_argument(
|
|
397
|
+
"provider",
|
|
398
|
+
nargs="?",
|
|
399
|
+
choices=["claude", "openai", "gemini"],
|
|
400
|
+
metavar="PROVIDER",
|
|
401
|
+
help="Provider to reset (omit to reset all)",
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
routing_subparsers.add_parser(
|
|
405
|
+
"init",
|
|
406
|
+
help="Create default routing.json configuration",
|
|
407
|
+
description="Generate .stravinsky/routing.json with default task-based routing rules.",
|
|
408
|
+
)
|
|
409
|
+
|
|
229
410
|
args = parser.parse_args()
|
|
230
411
|
|
|
231
412
|
if not args.command:
|
|
@@ -245,6 +426,16 @@ def main():
|
|
|
245
426
|
elif args.command == "init":
|
|
246
427
|
print(bootstrap_repo())
|
|
247
428
|
return 0
|
|
429
|
+
elif args.command == "routing":
|
|
430
|
+
if not args.routing_command:
|
|
431
|
+
routing_parser.print_help()
|
|
432
|
+
return 1
|
|
433
|
+
if args.routing_command == "status":
|
|
434
|
+
return cmd_routing_status()
|
|
435
|
+
elif args.routing_command == "reset":
|
|
436
|
+
return cmd_routing_reset(args.provider if hasattr(args, "provider") else None)
|
|
437
|
+
elif args.routing_command == "init":
|
|
438
|
+
return cmd_routing_init()
|
|
248
439
|
|
|
249
440
|
return 0
|
|
250
441
|
|
mcp_bridge/auth/oauth.py
CHANGED
|
@@ -13,13 +13,12 @@ import secrets
|
|
|
13
13
|
import threading
|
|
14
14
|
import webbrowser
|
|
15
15
|
from dataclasses import dataclass
|
|
16
|
-
from http.server import
|
|
16
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
17
17
|
from typing import Any
|
|
18
18
|
from urllib.parse import parse_qs, urlencode, urlparse
|
|
19
19
|
|
|
20
20
|
import httpx
|
|
21
21
|
|
|
22
|
-
|
|
23
22
|
# OAuth 2.0 Client Credentials (from constants.ts)
|
|
24
23
|
ANTIGRAVITY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
|
25
24
|
ANTIGRAVITY_CLIENT_SECRET = "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"
|
mcp_bridge/auth/openai_oauth.py
CHANGED
|
@@ -9,19 +9,16 @@ Port from: https://github.com/numman-ali/opencode-openai-codex-auth/blob/main/li
|
|
|
9
9
|
|
|
10
10
|
import base64
|
|
11
11
|
import hashlib
|
|
12
|
-
import json
|
|
13
12
|
import secrets
|
|
14
13
|
import threading
|
|
15
14
|
import webbrowser
|
|
16
15
|
from dataclasses import dataclass
|
|
17
|
-
from http.server import
|
|
16
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
17
|
from typing import Any
|
|
19
18
|
from urllib.parse import parse_qs, urlencode, urlparse
|
|
20
|
-
import time
|
|
21
19
|
|
|
22
20
|
import httpx
|
|
23
21
|
|
|
24
|
-
|
|
25
22
|
# OAuth constants (from openai/codex via opencode-openai-codex-auth)
|
|
26
23
|
CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
27
24
|
AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize" # Note: /oauth/authorize
|
|
@@ -303,8 +300,8 @@ def perform_oauth_flow(timeout: int = 300) -> TokenResult:
|
|
|
303
300
|
try:
|
|
304
301
|
auth_url, verifier, state = build_auth_url(redirect_uri)
|
|
305
302
|
|
|
306
|
-
print(
|
|
307
|
-
print(
|
|
303
|
+
print("\n🔐 Opening browser for OpenAI authentication...")
|
|
304
|
+
print("\nIf browser doesn't open, visit:")
|
|
308
305
|
print(f"{auth_url}\n")
|
|
309
306
|
|
|
310
307
|
webbrowser.open(auth_url)
|
|
@@ -329,7 +326,7 @@ def perform_oauth_flow(timeout: int = 300) -> TokenResult:
|
|
|
329
326
|
print("📝 Exchanging code for tokens...")
|
|
330
327
|
tokens = exchange_code(result["code"], verifier, redirect_uri)
|
|
331
328
|
|
|
332
|
-
print(
|
|
329
|
+
print("✓ Successfully authenticated with OpenAI")
|
|
333
330
|
|
|
334
331
|
return tokens
|
|
335
332
|
|
mcp_bridge/auth/token_store.py
CHANGED
|
@@ -9,9 +9,10 @@ Stores OAuth tokens securely using the OS keyring:
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import time
|
|
12
|
+
from pathlib import Path
|
|
12
13
|
from typing import TypedDict
|
|
13
14
|
|
|
14
|
-
import
|
|
15
|
+
from cryptography.fernet import Fernet
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class TokenData(TypedDict, total=False):
|
|
@@ -26,13 +27,15 @@ class TokenData(TypedDict, total=False):
|
|
|
26
27
|
|
|
27
28
|
class TokenStore:
|
|
28
29
|
"""
|
|
29
|
-
Secure storage for OAuth tokens using system keyring.
|
|
30
|
+
Secure storage for OAuth tokens using system keyring with encrypted file fallback.
|
|
30
31
|
|
|
31
32
|
Each provider (gemini, openai) stores its tokens separately.
|
|
32
33
|
Tokens are serialized as JSON for storage.
|
|
34
|
+
Falls back to encrypted file storage if keyring fails.
|
|
33
35
|
"""
|
|
34
36
|
|
|
35
37
|
SERVICE_NAME = "stravinsky"
|
|
38
|
+
FALLBACK_DIR = Path.home() / ".stravinsky" / "tokens"
|
|
36
39
|
|
|
37
40
|
def __init__(self, service_name: str | None = None):
|
|
38
41
|
"""Initialize the token store.
|
|
@@ -41,6 +44,63 @@ class TokenStore:
|
|
|
41
44
|
service_name: Override the default service name for testing.
|
|
42
45
|
"""
|
|
43
46
|
self.service_name = service_name or self.SERVICE_NAME
|
|
47
|
+
self._init_fallback_storage()
|
|
48
|
+
|
|
49
|
+
def _init_fallback_storage(self) -> None:
|
|
50
|
+
"""Initialize encrypted file storage fallback directory."""
|
|
51
|
+
self.FALLBACK_DIR.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
def _get_fallback_path(self, provider: str) -> Path:
|
|
54
|
+
"""Get the path for encrypted fallback storage."""
|
|
55
|
+
return self.FALLBACK_DIR / f"{provider}.enc"
|
|
56
|
+
|
|
57
|
+
def _get_or_create_key(self) -> bytes:
|
|
58
|
+
"""Get or create encryption key for fallback storage."""
|
|
59
|
+
key_file = self.FALLBACK_DIR / ".key"
|
|
60
|
+
if key_file.exists():
|
|
61
|
+
return key_file.read_bytes()
|
|
62
|
+
# Create new key and save it
|
|
63
|
+
key = Fernet.generate_key()
|
|
64
|
+
key_file.write_bytes(key)
|
|
65
|
+
key_file.chmod(0o600) # Read/write for owner only
|
|
66
|
+
return key
|
|
67
|
+
|
|
68
|
+
def _save_encrypted(self, provider: str, data: str) -> None:
|
|
69
|
+
"""Save data to encrypted file."""
|
|
70
|
+
try:
|
|
71
|
+
key = self._get_or_create_key()
|
|
72
|
+
cipher = Fernet(key)
|
|
73
|
+
encrypted = cipher.encrypt(data.encode())
|
|
74
|
+
path = self._get_fallback_path(provider)
|
|
75
|
+
path.write_bytes(encrypted)
|
|
76
|
+
path.chmod(0o600)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
raise RuntimeError(f"Failed to save encrypted token: {e}")
|
|
79
|
+
|
|
80
|
+
def _load_encrypted(self, provider: str) -> str | None:
|
|
81
|
+
"""Load data from encrypted file."""
|
|
82
|
+
try:
|
|
83
|
+
path = self._get_fallback_path(provider)
|
|
84
|
+
if not path.exists():
|
|
85
|
+
return None
|
|
86
|
+
key = self._get_or_create_key()
|
|
87
|
+
cipher = Fernet(key)
|
|
88
|
+
encrypted = path.read_bytes()
|
|
89
|
+
decrypted = cipher.decrypt(encrypted)
|
|
90
|
+
return decrypted.decode()
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def _delete_encrypted(self, provider: str) -> bool:
|
|
95
|
+
"""Delete encrypted token file."""
|
|
96
|
+
try:
|
|
97
|
+
path = self._get_fallback_path(provider)
|
|
98
|
+
if path.exists():
|
|
99
|
+
path.unlink()
|
|
100
|
+
return True
|
|
101
|
+
return False
|
|
102
|
+
except Exception:
|
|
103
|
+
return False
|
|
44
104
|
|
|
45
105
|
def _key(self, provider: str) -> str:
|
|
46
106
|
"""Generate the keyring key for a provider."""
|
|
@@ -56,13 +116,24 @@ class TokenStore:
|
|
|
56
116
|
Returns:
|
|
57
117
|
TokenData if found and valid, None otherwise.
|
|
58
118
|
"""
|
|
119
|
+
# Try keyring first (import inside try to catch backend initialization errors)
|
|
59
120
|
try:
|
|
121
|
+
import keyring
|
|
60
122
|
data = keyring.get_password(self.service_name, self._key(provider))
|
|
61
|
-
if
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
123
|
+
if data:
|
|
124
|
+
return json.loads(data)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass # Fall back to encrypted file (catches KeyringError, import errors, etc.)
|
|
127
|
+
|
|
128
|
+
# Fall back to encrypted file storage
|
|
129
|
+
try:
|
|
130
|
+
data = self._load_encrypted(provider)
|
|
131
|
+
if data:
|
|
132
|
+
return json.loads(data)
|
|
133
|
+
except json.JSONDecodeError:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
return None
|
|
66
137
|
|
|
67
138
|
def set_token(
|
|
68
139
|
self,
|
|
@@ -77,6 +148,7 @@ class TokenStore:
|
|
|
77
148
|
Store a token for a provider.
|
|
78
149
|
|
|
79
150
|
Can be called with a TokenData dict or individual parameters.
|
|
151
|
+
Falls back to encrypted file storage if keyring fails.
|
|
80
152
|
|
|
81
153
|
Args:
|
|
82
154
|
provider: The provider name (e.g., 'gemini', 'openai')
|
|
@@ -96,7 +168,27 @@ class TokenStore:
|
|
|
96
168
|
if expires_at:
|
|
97
169
|
token_data["expires_at"] = expires_at
|
|
98
170
|
data = json.dumps(token_data)
|
|
99
|
-
|
|
171
|
+
|
|
172
|
+
# Try keyring first, but always also write to encrypted storage
|
|
173
|
+
# Import inside try to catch backend initialization errors (e.g., "No recommended backend")
|
|
174
|
+
keyring_failed = False
|
|
175
|
+
try:
|
|
176
|
+
import keyring
|
|
177
|
+
keyring.set_password(self.service_name, self._key(provider), data)
|
|
178
|
+
except Exception:
|
|
179
|
+
# Keyring failed - fall back to encrypted file
|
|
180
|
+
keyring_failed = True
|
|
181
|
+
|
|
182
|
+
# Always write to encrypted file storage as fallback
|
|
183
|
+
try:
|
|
184
|
+
self._save_encrypted(provider, data)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
# Only fail if both backends failed
|
|
187
|
+
if keyring_failed:
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
f"Failed to save token to both keyring and encrypted storage: {e}"
|
|
190
|
+
)
|
|
191
|
+
|
|
100
192
|
|
|
101
193
|
def delete_token(self, provider: str) -> bool:
|
|
102
194
|
"""
|
|
@@ -108,11 +200,20 @@ class TokenStore:
|
|
|
108
200
|
Returns:
|
|
109
201
|
True if deleted, False if not found.
|
|
110
202
|
"""
|
|
203
|
+
# Try keyring first (import inside try to catch backend initialization errors)
|
|
204
|
+
deleted_from_keyring = False
|
|
111
205
|
try:
|
|
206
|
+
import keyring
|
|
112
207
|
keyring.delete_password(self.service_name, self._key(provider))
|
|
113
|
-
|
|
114
|
-
except
|
|
115
|
-
|
|
208
|
+
deleted_from_keyring = True
|
|
209
|
+
except Exception:
|
|
210
|
+
# Keyring failed (no backend, delete error, etc.) - continue to encrypted file
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
# Also delete from encrypted file storage
|
|
214
|
+
deleted_from_file = self._delete_encrypted(provider)
|
|
215
|
+
|
|
216
|
+
return deleted_from_keyring or deleted_from_file
|
|
116
217
|
|
|
117
218
|
def has_valid_token(self, provider: str) -> bool:
|
|
118
219
|
"""
|
mcp_bridge/cli/__init__.py
CHANGED