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/prompts/stravinsky.py
CHANGED
|
@@ -12,8 +12,6 @@ Key naming conventions (Stravinsky equivalents):
|
|
|
12
12
|
- agent_spawn (not call-omo-agent) - spawn background agents
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from typing import Optional
|
|
16
|
-
|
|
17
15
|
# Core role definition
|
|
18
16
|
STRAVINSKY_ROLE_SECTION = """<Role>
|
|
19
17
|
You are "Stravinsky" - Powerful AI Agent with orchestration capabilities from Stravinsky MCP.
|
|
@@ -36,7 +34,7 @@ Named after the composer known for revolutionary orchestration.
|
|
|
36
34
|
</Role>"""
|
|
37
35
|
|
|
38
36
|
|
|
39
|
-
STRAVINSKY_PHASE0_STEP1_3 = """### Step 0: Check Skills
|
|
37
|
+
STRAVINSKY_PHASE0_STEP1_3 = """### Step 0: Check Skills FUWT (BLOCKING)
|
|
40
38
|
|
|
41
39
|
**Before ANY classification or action, scan for matching skills.**
|
|
42
40
|
|
|
@@ -54,7 +52,7 @@ Skills are specialized workflows. When relevant, they handle the task better tha
|
|
|
54
52
|
|
|
55
53
|
| Type | Signal | Action |
|
|
56
54
|
|------|--------|--------|
|
|
57
|
-
| **Skill Match** | Matches skill trigger phrase | **INVOKE skill
|
|
55
|
+
| **Skill Match** | Matches skill trigger phrase | **INVOKE skill FUWT** via `skill_get` tool |
|
|
58
56
|
| **Trivial** | Single file, known location, direct answer | Direct tools only (UNLESS Key Trigger applies) |
|
|
59
57
|
| **Explicit** | Specific file/line, clear command | Execute directly |
|
|
60
58
|
| **Exploratory** | "How does X work?", "Find Y" | Fire explore (1-3) + tools in parallel |
|
|
@@ -159,7 +157,7 @@ STOP searching when:
|
|
|
159
157
|
**DO NOT over-explore. Time is precious.**"""
|
|
160
158
|
|
|
161
159
|
|
|
162
|
-
STRAVINSKY_PHASE2B_PRE_IMPLEMENTATION = """## ⚠️ CRITICAL: PARALLEL-
|
|
160
|
+
STRAVINSKY_PHASE2B_PRE_IMPLEMENTATION = """## ⚠️ CRITICAL: PARALLEL-FUWT WORKFLOW
|
|
163
161
|
|
|
164
162
|
**BLOCKING REQUIREMENT**: For implementation tasks, your response structure MUST be:
|
|
165
163
|
|
|
@@ -204,54 +202,78 @@ todowrite([todo1, todo2, todo3])
|
|
|
204
202
|
4. THEN mark todos complete"""
|
|
205
203
|
|
|
206
204
|
|
|
207
|
-
STRAVINSKY_DELEGATION_PROMPT_STRUCTURE = """### Delegation Prompt Structure (
|
|
205
|
+
STRAVINSKY_DELEGATION_PROMPT_STRUCTURE = """### Delegation Prompt Structure (RECOMMENDED - 5 KEY SECTIONS):
|
|
208
206
|
|
|
209
|
-
When delegating via `agent_spawn`, your prompt
|
|
207
|
+
When delegating via `agent_spawn`, your prompt SHOULD include these sections:
|
|
210
208
|
|
|
211
209
|
```
|
|
212
|
-
1. TASK:
|
|
210
|
+
1. TASK: Clear, natural language description of what needs to be found/analyzed
|
|
213
211
|
2. EXPECTED OUTCOME: Concrete deliverables with success criteria
|
|
214
|
-
3.
|
|
215
|
-
4. MUST DO:
|
|
216
|
-
5.
|
|
217
|
-
6. CONTEXT: File paths, existing patterns, constraints
|
|
218
|
-
7. SUCCESS CRITERIA: How to verify completion
|
|
212
|
+
3. MUST DO: Exhaustive requirements list
|
|
213
|
+
4. MUST NOT DO: Forbidden actions (prevent rogue behavior)
|
|
214
|
+
5. CONTEXT: File paths, existing patterns, constraints (if known)
|
|
219
215
|
```
|
|
220
216
|
|
|
221
|
-
|
|
217
|
+
**❌ WRONG - Over-Prescribing Tools:**
|
|
222
218
|
```
|
|
223
219
|
## TASK
|
|
224
220
|
Find all API endpoint definitions in the auth module.
|
|
225
221
|
|
|
226
|
-
## EXPECTED OUTCOME
|
|
227
|
-
List of endpoints with: path, method, handler function, file location.
|
|
228
|
-
|
|
229
222
|
## REQUIRED TOOLS
|
|
230
|
-
Read, Grep, Glob
|
|
223
|
+
Read, Grep, Glob # ❌ DON'T PRESCRIBE TOOLS - agents choose optimal approach
|
|
224
|
+
|
|
225
|
+
## MUST DO
|
|
226
|
+
- Use grep_search to find "def" patterns # ❌ TOO SPECIFIC
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**✅ CORRECT - Natural Language + Trust Agent Intelligence:**
|
|
230
|
+
```
|
|
231
|
+
## TASK
|
|
232
|
+
Find and explain all API endpoint definitions in the auth module, including their request/response patterns and how they connect to each other.
|
|
233
|
+
|
|
234
|
+
## EXPECTED OUTCOME
|
|
235
|
+
Complete list of endpoints with: path, method, handler function, file location, and architectural notes on how they integrate.
|
|
231
236
|
|
|
232
237
|
## MUST DO
|
|
233
|
-
- Search in src/auth/ directory
|
|
234
|
-
- Include path parameters
|
|
235
|
-
- Report line numbers
|
|
238
|
+
- Search in src/auth/ directory and related integration points
|
|
239
|
+
- Include path parameters and query string handling
|
|
240
|
+
- Report exact line numbers for each endpoint
|
|
241
|
+
- Explain the authentication flow across endpoints
|
|
236
242
|
|
|
237
243
|
## MUST NOT DO
|
|
238
244
|
- Modify any files
|
|
239
|
-
-
|
|
245
|
+
- Skip integration patterns (how endpoints call each other)
|
|
240
246
|
|
|
241
247
|
## CONTEXT
|
|
242
248
|
Project uses FastAPI. Auth endpoints handle login, logout, token refresh.
|
|
249
|
+
This is a CONCEPTUAL/ARCHITECTURAL query - the agent should use semantic_search + grep for comprehensive coverage.
|
|
243
250
|
|
|
244
251
|
## SUCCESS CRITERIA
|
|
245
|
-
All endpoints documented with complete paths
|
|
252
|
+
All endpoints documented with complete paths, handlers, AND architectural understanding of how they work together.
|
|
246
253
|
```
|
|
247
254
|
|
|
255
|
+
**WHY THIS WORKS BETTER:**
|
|
256
|
+
- ✅ Explore agent has `semantic_search` in its toolset and knows when to use it
|
|
257
|
+
- ✅ Natural language tasks → agent classifies as SEMANTIC → uses semantic_search
|
|
258
|
+
- ✅ Agent combines semantic_search (concepts) + grep_search (exact matches) automatically
|
|
259
|
+
- ❌ "REQUIRED TOOLS: grep_search" → blocks semantic_search even for conceptual queries
|
|
260
|
+
|
|
261
|
+
**TRUST THE AGENTS:**
|
|
262
|
+
The explore agent already has comprehensive tool selection logic:
|
|
263
|
+
- Semantic queries → semantic_search (primary)
|
|
264
|
+
- Exact syntax → grep_search
|
|
265
|
+
- Code structure → ast_grep_search
|
|
266
|
+
- Symbol navigation → LSP tools
|
|
267
|
+
|
|
268
|
+
Let them choose the optimal search strategy based on your TASK description, not prescriptive tool lists.
|
|
269
|
+
|
|
248
270
|
AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS:
|
|
249
271
|
- DOES IT WORK AS EXPECTED?
|
|
250
272
|
- DOES IT FOLLOW THE EXISTING CODEBASE PATTERN?
|
|
251
273
|
- EXPECTED RESULT CAME OUT?
|
|
252
274
|
- DID THE AGENT FOLLOW "MUST DO" AND "MUST NOT DO" REQUIREMENTS?
|
|
253
275
|
|
|
254
|
-
**
|
|
276
|
+
**Natural language task descriptions = agent intelligence. Tool prescriptions = micromanagement.**"""
|
|
255
277
|
|
|
256
278
|
|
|
257
279
|
STRAVINSKY_GITHUB_WORKFLOW = """### GitHub Workflow (CRITICAL - When mentioned in issues/PRs):
|
|
@@ -358,7 +380,7 @@ If verification fails:
|
|
|
358
380
|
|
|
359
381
|
STRAVINSKY_KEY_TRIGGERS = """### Key Triggers (check BEFORE classification):
|
|
360
382
|
|
|
361
|
-
**BLOCKING: Check skills
|
|
383
|
+
**BLOCKING: Check skills FUWT before any action.**
|
|
362
384
|
If a skill matches, invoke it IMMEDIATELY via `skill_get` tool.
|
|
363
385
|
|
|
364
386
|
- External library/source mentioned -> fire `dewey` background via `agent_spawn`
|
|
@@ -472,12 +494,12 @@ Delphi is an expensive, high-quality reasoning model. Use it wisely via `agent_s
|
|
|
472
494
|
|
|
473
495
|
| Trigger | Action |
|
|
474
496
|
|---------|--------|
|
|
475
|
-
| Complex architecture design | Delphi
|
|
476
|
-
| After completing significant work | Delphi
|
|
477
|
-
| 2+ failed fix attempts | Delphi
|
|
478
|
-
| Unfamiliar code patterns | Delphi
|
|
479
|
-
| Security/performance concerns | Delphi
|
|
480
|
-
| Multi-system tradeoffs | Delphi
|
|
497
|
+
| Complex architecture design | Delphi FUWT, then implement |
|
|
498
|
+
| After completing significant work | Delphi FUWT, then implement |
|
|
499
|
+
| 2+ failed fix attempts | Delphi FUWT, then implement |
|
|
500
|
+
| Unfamiliar code patterns | Delphi FUWT, then implement |
|
|
501
|
+
| Security/performance concerns | Delphi FUWT, then implement |
|
|
502
|
+
| Multi-system tradeoffs | Delphi FUWT, then implement |
|
|
481
503
|
|
|
482
504
|
### WHEN NOT to Consult:
|
|
483
505
|
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import httpx
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("stravinsky.proxy_client")
|
|
7
|
+
|
|
8
|
+
PROXY_URL = os.getenv("STRAVINSKY_PROXY_URL", "http://127.0.0.1:8765")
|
|
9
|
+
|
|
10
|
+
async def proxy_invoke_gemini(
|
|
11
|
+
prompt: str,
|
|
12
|
+
model: str = "gemini-3-flash",
|
|
13
|
+
temperature: float = 0.7,
|
|
14
|
+
max_tokens: int = 8192,
|
|
15
|
+
thinking_budget: int = 0,
|
|
16
|
+
image_path: Optional[str] = None,
|
|
17
|
+
agent_context: Optional[dict[str, Any]] = None
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Routes Gemini invocation to the proxy server."""
|
|
20
|
+
payload = {
|
|
21
|
+
"prompt": prompt,
|
|
22
|
+
"model": model,
|
|
23
|
+
"temperature": temperature,
|
|
24
|
+
"max_tokens": max_tokens,
|
|
25
|
+
"thinking_budget": thinking_budget,
|
|
26
|
+
"image_path": image_path,
|
|
27
|
+
"agent_context": agent_context
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async with httpx.AsyncClient(timeout=130.0) as client:
|
|
31
|
+
try:
|
|
32
|
+
response = await client.post(f"{PROXY_URL}/v1/gemini/generate", json=payload)
|
|
33
|
+
response.raise_for_status()
|
|
34
|
+
return response.json()["response"]
|
|
35
|
+
except Exception as e:
|
|
36
|
+
logger.error(f"Proxy request failed: {e}")
|
|
37
|
+
raise
|
|
38
|
+
|
|
39
|
+
async def proxy_invoke_openai(
|
|
40
|
+
prompt: str,
|
|
41
|
+
model: str = "gpt-5.2-codex",
|
|
42
|
+
temperature: float = 0.7,
|
|
43
|
+
max_tokens: int = 4096,
|
|
44
|
+
thinking_budget: int = 0,
|
|
45
|
+
reasoning_effort: str = "medium",
|
|
46
|
+
agent_context: Optional[dict[str, Any]] = None
|
|
47
|
+
) -> str:
|
|
48
|
+
"""Routes OpenAI invocation to the proxy server."""
|
|
49
|
+
payload = {
|
|
50
|
+
"prompt": prompt,
|
|
51
|
+
"model": model,
|
|
52
|
+
"temperature": temperature,
|
|
53
|
+
"max_tokens": max_tokens,
|
|
54
|
+
"thinking_budget": thinking_budget,
|
|
55
|
+
"reasoning_effort": reasoning_effort,
|
|
56
|
+
"agent_context": agent_context
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async with httpx.AsyncClient(timeout=130.0) as client:
|
|
60
|
+
try:
|
|
61
|
+
response = await client.post(f"{PROXY_URL}/v1/openai/chat", json=payload)
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
return response.json()["response"]
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Proxy request failed: {e}")
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
def is_proxy_enabled() -> bool:
|
|
69
|
+
"""Checks if proxy usage is enabled via environment variable."""
|
|
70
|
+
return os.getenv("STRAVINSKY_USE_PROXY", "false").lower() == "true"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import uvicorn
|
|
8
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from mcp_bridge.auth.token_store import TokenStore
|
|
12
|
+
from mcp_bridge.tools.model_invoke import invoke_gemini, invoke_openai
|
|
13
|
+
|
|
14
|
+
# Configure logging
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
18
|
+
)
|
|
19
|
+
logger = logging.getLogger("stravinsky.proxy")
|
|
20
|
+
|
|
21
|
+
app = FastAPI(title="Stravinsky Model Proxy")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.middleware("http")
|
|
25
|
+
async def add_process_time_header(request: Request, call_next):
|
|
26
|
+
request_id = str(uuid.uuid4())
|
|
27
|
+
start_time = time.time()
|
|
28
|
+
logger.info(f"[{request_id}] {request.method} {request.url.path}")
|
|
29
|
+
response = await call_next(request)
|
|
30
|
+
process_time = time.time() - start_time
|
|
31
|
+
response.headers["X-Process-Time"] = str(process_time)
|
|
32
|
+
response.headers["X-Request-ID"] = request_id
|
|
33
|
+
logger.info(f"[{request_id}] Completed in {process_time:.4f}s")
|
|
34
|
+
return response
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Shared token store
|
|
38
|
+
_token_store = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_token_store():
|
|
42
|
+
global _token_store
|
|
43
|
+
if _token_store is None:
|
|
44
|
+
_token_store = TokenStore()
|
|
45
|
+
return _token_store
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class GeminiRequest(BaseModel):
|
|
49
|
+
prompt: str
|
|
50
|
+
model: str = "gemini-3-flash"
|
|
51
|
+
temperature: float = 0.7
|
|
52
|
+
max_tokens: int = 8192
|
|
53
|
+
thinking_budget: int = 0
|
|
54
|
+
image_path: str | None = None
|
|
55
|
+
agent_context: dict[str, Any] | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class GeminiAgenticRequest(BaseModel):
|
|
59
|
+
prompt: str
|
|
60
|
+
model: str = "gemini-3-flash"
|
|
61
|
+
max_turns: int = 10
|
|
62
|
+
timeout: int = 120
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class OpenAIRequest(BaseModel):
|
|
66
|
+
prompt: str
|
|
67
|
+
model: str = "gpt-5.2-codex"
|
|
68
|
+
temperature: float = 0.7
|
|
69
|
+
max_tokens: int = 4096
|
|
70
|
+
thinking_budget: int = 0
|
|
71
|
+
reasoning_effort: str = "medium"
|
|
72
|
+
agent_context: dict[str, Any] | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.get("/health")
|
|
76
|
+
async def health():
|
|
77
|
+
return {"status": "ok"}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.post("/v1/gemini/generate")
|
|
81
|
+
async def gemini_generate(request: GeminiRequest):
|
|
82
|
+
"""Proxy endpoint for Gemini generation."""
|
|
83
|
+
try:
|
|
84
|
+
token_store = get_token_store()
|
|
85
|
+
# We need to ensure agent_context is passed correctly if invoke_gemini supports it
|
|
86
|
+
# Based on previous read, invoke_gemini takes image_path, but agent_context
|
|
87
|
+
# might be extracted from hooks or passed in params.
|
|
88
|
+
# Actually, invoke_gemini extracts it from params.
|
|
89
|
+
|
|
90
|
+
response = await invoke_gemini(
|
|
91
|
+
token_store=token_store,
|
|
92
|
+
prompt=request.prompt,
|
|
93
|
+
model=request.model,
|
|
94
|
+
temperature=request.temperature,
|
|
95
|
+
max_tokens=request.max_tokens,
|
|
96
|
+
thinking_budget=request.thinking_budget,
|
|
97
|
+
image_path=request.image_path,
|
|
98
|
+
)
|
|
99
|
+
return {"response": response}
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Error in gemini_generate proxy: {e}", exc_info=True)
|
|
102
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.post("/v1/gemini/agentic")
|
|
106
|
+
async def gemini_agentic(request: GeminiAgenticRequest):
|
|
107
|
+
"""Proxy endpoint for Agentic Gemini."""
|
|
108
|
+
try:
|
|
109
|
+
from mcp_bridge.tools.model_invoke import invoke_gemini_agentic
|
|
110
|
+
|
|
111
|
+
token_store = get_token_store()
|
|
112
|
+
response = await invoke_gemini_agentic(
|
|
113
|
+
token_store=token_store,
|
|
114
|
+
prompt=request.prompt,
|
|
115
|
+
model=request.model,
|
|
116
|
+
max_turns=request.max_turns,
|
|
117
|
+
timeout=request.timeout,
|
|
118
|
+
)
|
|
119
|
+
return {"response": response}
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error in gemini_agentic proxy: {e}", exc_info=True)
|
|
122
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.post("/v1/openai/chat")
|
|
126
|
+
async def openai_chat(request: OpenAIRequest):
|
|
127
|
+
"""Proxy endpoint for OpenAI chat."""
|
|
128
|
+
try:
|
|
129
|
+
token_store = get_token_store()
|
|
130
|
+
response = await invoke_openai(
|
|
131
|
+
token_store=token_store,
|
|
132
|
+
prompt=request.prompt,
|
|
133
|
+
model=request.model,
|
|
134
|
+
temperature=request.temperature,
|
|
135
|
+
max_tokens=request.max_tokens,
|
|
136
|
+
thinking_budget=request.thinking_budget,
|
|
137
|
+
reasoning_effort=request.reasoning_effort,
|
|
138
|
+
)
|
|
139
|
+
return {"response": response}
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Error in openai_chat proxy: {e}", exc_info=True)
|
|
142
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main():
|
|
146
|
+
"""Entry point for the proxy server."""
|
|
147
|
+
# CRITICAL: Disable proxy usage within the proxy process to avoid infinite loops
|
|
148
|
+
os.environ["STRAVINSKY_USE_PROXY"] = "false"
|
|
149
|
+
|
|
150
|
+
port = int(os.getenv("STRAVINSKY_PROXY_PORT", 8765))
|
|
151
|
+
host = os.getenv("STRAVINSKY_PROXY_HOST", "127.0.0.1")
|
|
152
|
+
logger.info(f"Starting Stravinsky Model Proxy on {host}:{port}")
|
|
153
|
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stravinsky Multi-Provider Routing System.
|
|
3
|
+
|
|
4
|
+
This module provides intelligent routing between providers (Claude, OpenAI, Gemini)
|
|
5
|
+
with automatic fallback when providers hit rate limits or capacity constraints.
|
|
6
|
+
|
|
7
|
+
Components:
|
|
8
|
+
- ProviderState: Tracks availability of each provider
|
|
9
|
+
- ProviderStateTracker: Global state management for all providers
|
|
10
|
+
- TaskClassifier: Classifies tasks to route to optimal providers
|
|
11
|
+
- RoutingConfig: Project-local configuration loader
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .config import (
|
|
15
|
+
DEFAULT_ROUTING_CONFIG,
|
|
16
|
+
RoutingConfig,
|
|
17
|
+
load_routing_config,
|
|
18
|
+
)
|
|
19
|
+
from .provider_state import (
|
|
20
|
+
ProviderState,
|
|
21
|
+
ProviderStateTracker,
|
|
22
|
+
get_provider_tracker,
|
|
23
|
+
)
|
|
24
|
+
from .task_classifier import (
|
|
25
|
+
TaskType,
|
|
26
|
+
classify_task,
|
|
27
|
+
get_routing_for_task,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
# Provider state
|
|
32
|
+
"ProviderState",
|
|
33
|
+
"ProviderStateTracker",
|
|
34
|
+
"get_provider_tracker",
|
|
35
|
+
# Task classification
|
|
36
|
+
"TaskType",
|
|
37
|
+
"classify_task",
|
|
38
|
+
"get_routing_for_task",
|
|
39
|
+
# Configuration
|
|
40
|
+
"load_routing_config",
|
|
41
|
+
"RoutingConfig",
|
|
42
|
+
"DEFAULT_ROUTING_CONFIG",
|
|
43
|
+
]
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Routing Configuration with Project-Local Priority.
|
|
3
|
+
|
|
4
|
+
Loads routing configuration from:
|
|
5
|
+
1. .stravinsky/routing.json (project-local - highest priority)
|
|
6
|
+
2. ~/.stravinsky/routing.json (user-global fallback)
|
|
7
|
+
3. Built-in defaults
|
|
8
|
+
|
|
9
|
+
This allows per-project customization of routing behavior.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Literal
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Default routing configuration
|
|
23
|
+
DEFAULT_ROUTING_CONFIG: dict[str, Any] = {
|
|
24
|
+
"routing": {
|
|
25
|
+
"enabled": True,
|
|
26
|
+
"task_routing": {
|
|
27
|
+
"code_generation": {
|
|
28
|
+
"provider": "claude",
|
|
29
|
+
"model": "claude-4.5-opus",
|
|
30
|
+
"tier": "premium",
|
|
31
|
+
},
|
|
32
|
+
"code_refactoring": {
|
|
33
|
+
"provider": "claude",
|
|
34
|
+
"model": "claude-4.5-sonnet",
|
|
35
|
+
"tier": "standard",
|
|
36
|
+
},
|
|
37
|
+
"debugging": {
|
|
38
|
+
"provider": "openai",
|
|
39
|
+
"model": "gpt-5.2",
|
|
40
|
+
"tier": "standard",
|
|
41
|
+
},
|
|
42
|
+
"architecture": {
|
|
43
|
+
"provider": "openai",
|
|
44
|
+
"model": "gpt-5.2",
|
|
45
|
+
"tier": "standard",
|
|
46
|
+
},
|
|
47
|
+
"documentation": {
|
|
48
|
+
"provider": "gemini",
|
|
49
|
+
"model": "gemini-3-flash-preview",
|
|
50
|
+
"tier": "standard",
|
|
51
|
+
},
|
|
52
|
+
"code_search": {
|
|
53
|
+
"provider": "gemini",
|
|
54
|
+
"model": "gemini-3-flash-preview",
|
|
55
|
+
"tier": "standard",
|
|
56
|
+
},
|
|
57
|
+
"security_review": {
|
|
58
|
+
"provider": "claude",
|
|
59
|
+
"model": "claude-4.5-opus",
|
|
60
|
+
"tier": "premium",
|
|
61
|
+
},
|
|
62
|
+
"general": {
|
|
63
|
+
"provider": "claude",
|
|
64
|
+
"model": "claude-4.5-sonnet",
|
|
65
|
+
"tier": "standard",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
"fallback": {
|
|
69
|
+
"enabled": True,
|
|
70
|
+
"chain": ["claude", "openai", "gemini"],
|
|
71
|
+
"cooldown_seconds": 300,
|
|
72
|
+
},
|
|
73
|
+
"claude_limits": {
|
|
74
|
+
"detection_enabled": True,
|
|
75
|
+
"slow_response_threshold_seconds": 30,
|
|
76
|
+
"auto_fallback": True,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class TaskRoutingRule:
|
|
84
|
+
"""Routing rule for a specific task type."""
|
|
85
|
+
|
|
86
|
+
provider: str
|
|
87
|
+
model: str | None = None
|
|
88
|
+
tier: Literal["premium", "standard"] = "standard"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class FallbackConfig:
|
|
93
|
+
"""Fallback configuration."""
|
|
94
|
+
|
|
95
|
+
enabled: bool = True
|
|
96
|
+
chain: list[str] = field(default_factory=lambda: ["claude", "openai", "gemini"])
|
|
97
|
+
cooldown_seconds: int = 300
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class ClaudeLimitsConfig:
|
|
102
|
+
"""Claude limits detection configuration."""
|
|
103
|
+
|
|
104
|
+
detection_enabled: bool = True
|
|
105
|
+
slow_response_threshold_seconds: int = 30
|
|
106
|
+
auto_fallback: bool = True
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class RoutingConfig:
|
|
111
|
+
"""Complete routing configuration."""
|
|
112
|
+
|
|
113
|
+
enabled: bool = True
|
|
114
|
+
task_routing: dict[str, TaskRoutingRule] = field(default_factory=dict)
|
|
115
|
+
fallback: FallbackConfig = field(default_factory=FallbackConfig)
|
|
116
|
+
claude_limits: ClaudeLimitsConfig = field(default_factory=ClaudeLimitsConfig)
|
|
117
|
+
source: str = "default" # Where config was loaded from
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_dict(cls, data: dict[str, Any], source: str = "dict") -> RoutingConfig:
|
|
121
|
+
"""Create RoutingConfig from a dictionary."""
|
|
122
|
+
routing = data.get("routing", data) # Handle both wrapped and unwrapped
|
|
123
|
+
|
|
124
|
+
# Parse task routing
|
|
125
|
+
task_routing = {}
|
|
126
|
+
for task_type, rule in routing.get("task_routing", {}).items():
|
|
127
|
+
if isinstance(rule, dict):
|
|
128
|
+
task_routing[task_type] = TaskRoutingRule(
|
|
129
|
+
provider=rule.get("provider", "claude"),
|
|
130
|
+
model=rule.get("model"),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Parse fallback config
|
|
134
|
+
fallback_data = routing.get("fallback", {})
|
|
135
|
+
fallback = FallbackConfig(
|
|
136
|
+
enabled=fallback_data.get("enabled", True),
|
|
137
|
+
chain=fallback_data.get("chain", ["claude", "openai", "gemini"]),
|
|
138
|
+
cooldown_seconds=fallback_data.get("cooldown_seconds", 300),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Parse claude limits config
|
|
142
|
+
claude_data = routing.get("claude_limits", {})
|
|
143
|
+
claude_limits = ClaudeLimitsConfig(
|
|
144
|
+
detection_enabled=claude_data.get("detection_enabled", True),
|
|
145
|
+
slow_response_threshold_seconds=claude_data.get("slow_response_threshold_seconds", 30),
|
|
146
|
+
auto_fallback=claude_data.get("auto_fallback", True),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return cls(
|
|
150
|
+
enabled=routing.get("enabled", True),
|
|
151
|
+
task_routing=task_routing,
|
|
152
|
+
fallback=fallback,
|
|
153
|
+
claude_limits=claude_limits,
|
|
154
|
+
source=source,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def get_routing_for_task(self, task_type: str) -> TaskRoutingRule:
|
|
158
|
+
"""Get routing rule for a task type, with fallback to general."""
|
|
159
|
+
if task_type in self.task_routing:
|
|
160
|
+
return self.task_routing[task_type]
|
|
161
|
+
if "general" in self.task_routing:
|
|
162
|
+
return self.task_routing["general"]
|
|
163
|
+
return TaskRoutingRule(provider="claude", model=None)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def load_routing_config(project_path: str = ".") -> RoutingConfig:
|
|
167
|
+
"""
|
|
168
|
+
Load routing config with project-local priority.
|
|
169
|
+
|
|
170
|
+
Discovery order:
|
|
171
|
+
1. .stravinsky/routing.json (project-local)
|
|
172
|
+
2. ~/.stravinsky/routing.json (user-global)
|
|
173
|
+
3. Built-in defaults
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
project_path: Path to the project root
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
RoutingConfig instance
|
|
180
|
+
"""
|
|
181
|
+
# Project-local config
|
|
182
|
+
project_config_path = Path(project_path) / ".stravinsky" / "routing.json"
|
|
183
|
+
if project_config_path.exists():
|
|
184
|
+
try:
|
|
185
|
+
data = json.loads(project_config_path.read_text())
|
|
186
|
+
config = RoutingConfig.from_dict(data, source=str(project_config_path))
|
|
187
|
+
logger.info(f"[RoutingConfig] Loaded project-local config from {project_config_path}")
|
|
188
|
+
return config
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.warning(f"[RoutingConfig] Failed to load {project_config_path}: {e}")
|
|
191
|
+
|
|
192
|
+
# User-global fallback
|
|
193
|
+
global_config_path = Path.home() / ".stravinsky" / "routing.json"
|
|
194
|
+
if global_config_path.exists():
|
|
195
|
+
try:
|
|
196
|
+
data = json.loads(global_config_path.read_text())
|
|
197
|
+
config = RoutingConfig.from_dict(data, source=str(global_config_path))
|
|
198
|
+
logger.info(f"[RoutingConfig] Loaded user-global config from {global_config_path}")
|
|
199
|
+
return config
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.warning(f"[RoutingConfig] Failed to load {global_config_path}: {e}")
|
|
202
|
+
|
|
203
|
+
# Built-in defaults
|
|
204
|
+
logger.info("[RoutingConfig] Using built-in defaults")
|
|
205
|
+
return RoutingConfig.from_dict(DEFAULT_ROUTING_CONFIG, source="default")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def init_routing_config(project_path: str = ".") -> Path:
|
|
209
|
+
"""
|
|
210
|
+
Initialize a project-local routing config file.
|
|
211
|
+
|
|
212
|
+
Creates .stravinsky/routing.json with default configuration.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
project_path: Path to the project root
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Path to the created config file
|
|
219
|
+
"""
|
|
220
|
+
config_dir = Path(project_path) / ".stravinsky"
|
|
221
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
|
|
223
|
+
config_path = config_dir / "routing.json"
|
|
224
|
+
|
|
225
|
+
if config_path.exists():
|
|
226
|
+
logger.warning(f"[RoutingConfig] Config already exists at {config_path}")
|
|
227
|
+
return config_path
|
|
228
|
+
|
|
229
|
+
config_path.write_text(json.dumps(DEFAULT_ROUTING_CONFIG, indent=2))
|
|
230
|
+
logger.info(f"[RoutingConfig] Created config at {config_path}")
|
|
231
|
+
|
|
232
|
+
return config_path
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_config_source(project_path: str = ".") -> str:
|
|
236
|
+
"""
|
|
237
|
+
Get the source of the active routing config.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Path to the config file being used, or "default" for built-in
|
|
241
|
+
"""
|
|
242
|
+
project_config = Path(project_path) / ".stravinsky" / "routing.json"
|
|
243
|
+
if project_config.exists():
|
|
244
|
+
return str(project_config)
|
|
245
|
+
|
|
246
|
+
global_config = Path.home() / ".stravinsky" / "routing.json"
|
|
247
|
+
if global_config.exists():
|
|
248
|
+
return str(global_config)
|
|
249
|
+
|
|
250
|
+
return "default"
|