stravinsky 0.1.12__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 +5 -0
- mcp_bridge/auth/__init__.py +32 -0
- mcp_bridge/auth/cli.py +208 -0
- mcp_bridge/auth/oauth.py +418 -0
- mcp_bridge/auth/openai_oauth.py +350 -0
- mcp_bridge/auth/token_store.py +195 -0
- mcp_bridge/config/__init__.py +14 -0
- mcp_bridge/config/hooks.py +174 -0
- mcp_bridge/prompts/__init__.py +18 -0
- mcp_bridge/prompts/delphi.py +110 -0
- mcp_bridge/prompts/dewey.py +183 -0
- mcp_bridge/prompts/document_writer.py +155 -0
- mcp_bridge/prompts/explore.py +118 -0
- mcp_bridge/prompts/frontend.py +112 -0
- mcp_bridge/prompts/multimodal.py +58 -0
- mcp_bridge/prompts/stravinsky.py +329 -0
- mcp_bridge/server.py +866 -0
- mcp_bridge/tools/__init__.py +31 -0
- mcp_bridge/tools/agent_manager.py +665 -0
- mcp_bridge/tools/background_tasks.py +166 -0
- mcp_bridge/tools/code_search.py +301 -0
- mcp_bridge/tools/continuous_loop.py +67 -0
- mcp_bridge/tools/lsp/__init__.py +29 -0
- mcp_bridge/tools/lsp/tools.py +526 -0
- mcp_bridge/tools/model_invoke.py +233 -0
- mcp_bridge/tools/project_context.py +141 -0
- mcp_bridge/tools/session_manager.py +302 -0
- mcp_bridge/tools/skill_loader.py +212 -0
- mcp_bridge/tools/task_runner.py +97 -0
- mcp_bridge/utils/__init__.py +1 -0
- stravinsky-0.1.12.dist-info/METADATA +198 -0
- stravinsky-0.1.12.dist-info/RECORD +34 -0
- stravinsky-0.1.12.dist-info/WHEEL +4 -0
- stravinsky-0.1.12.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Background Task Manager for Stravinsky.
|
|
3
|
+
|
|
4
|
+
Provides mechanisms to spawn, monitor, and manage async sub-agents.
|
|
5
|
+
Tasks are persisted to .stravinsky/tasks.json.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
from dataclasses import asdict, dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class BackgroundTask:
|
|
23
|
+
id: str
|
|
24
|
+
prompt: str
|
|
25
|
+
model: str
|
|
26
|
+
status: str # pending, running, completed, failed
|
|
27
|
+
created_at: str
|
|
28
|
+
started_at: Optional[str] = None
|
|
29
|
+
completed_at: Optional[str] = None
|
|
30
|
+
result: Optional[str] = None
|
|
31
|
+
error: Optional[str] = None
|
|
32
|
+
pid: Optional[int] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BackgroundManager:
|
|
36
|
+
def __init__(self, base_dir: Optional[str] = None):
|
|
37
|
+
if base_dir:
|
|
38
|
+
self.base_dir = Path(base_dir)
|
|
39
|
+
else:
|
|
40
|
+
# Default to .stravinsky in the current working directory
|
|
41
|
+
self.base_dir = Path.cwd() / ".stravinsky"
|
|
42
|
+
|
|
43
|
+
self.tasks_dir = self.base_dir / "tasks"
|
|
44
|
+
self.state_file = self.base_dir / "tasks.json"
|
|
45
|
+
|
|
46
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
self.tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
if not self.state_file.exists():
|
|
50
|
+
self._save_tasks({})
|
|
51
|
+
|
|
52
|
+
def _load_tasks(self) -> Dict[str, Any]:
|
|
53
|
+
try:
|
|
54
|
+
with open(self.state_file, "r") as f:
|
|
55
|
+
return json.load(f)
|
|
56
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
def _save_tasks(self, tasks: Dict[str, Any]):
|
|
60
|
+
with open(self.state_file, "w") as f:
|
|
61
|
+
json.dump(tasks, f, indent=2)
|
|
62
|
+
|
|
63
|
+
def create_task(self, prompt: str, model: str) -> str:
|
|
64
|
+
task_id = str(uuid.uuid4())[:8]
|
|
65
|
+
task = BackgroundTask(
|
|
66
|
+
id=task_id,
|
|
67
|
+
prompt=prompt,
|
|
68
|
+
model=model,
|
|
69
|
+
status="pending",
|
|
70
|
+
created_at=datetime.isoformat(datetime.now()),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
tasks = self._load_tasks()
|
|
74
|
+
tasks[task_id] = asdict(task)
|
|
75
|
+
self._save_tasks(tasks)
|
|
76
|
+
return task_id
|
|
77
|
+
|
|
78
|
+
def update_task(self, task_id: str, **kwargs):
|
|
79
|
+
tasks = self._load_tasks()
|
|
80
|
+
if task_id in tasks:
|
|
81
|
+
tasks[task_id].update(kwargs)
|
|
82
|
+
self._save_tasks(tasks)
|
|
83
|
+
|
|
84
|
+
def get_task(self, task_id: str) -> Optional[Dict[str, Any]]:
|
|
85
|
+
tasks = self._load_tasks()
|
|
86
|
+
return tasks.get(task_id)
|
|
87
|
+
|
|
88
|
+
def list_tasks(self) -> List[Dict[str, Any]]:
|
|
89
|
+
tasks = self._load_tasks()
|
|
90
|
+
return list(tasks.values())
|
|
91
|
+
|
|
92
|
+
def spawn(self, task_id: str):
|
|
93
|
+
"""
|
|
94
|
+
Spawns a background process to execute the task.
|
|
95
|
+
In this implementation, it uses another instance of the MCP bridge
|
|
96
|
+
or a dedicated tool invoker script.
|
|
97
|
+
"""
|
|
98
|
+
task = self.get_task(task_id)
|
|
99
|
+
if not task:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# We'll use a wrapper script that handles the model invocation and updates the status
|
|
103
|
+
# mcp_bridge/tools/task_runner.py (we will create this)
|
|
104
|
+
|
|
105
|
+
log_file = self.tasks_dir / f"{task_id}.log"
|
|
106
|
+
|
|
107
|
+
# Start the background process
|
|
108
|
+
cmd = [
|
|
109
|
+
sys.executable,
|
|
110
|
+
"-m", "mcp_bridge.tools.task_runner",
|
|
111
|
+
"--task-id", task_id,
|
|
112
|
+
"--base-dir", str(self.base_dir)
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Using Popen to run in background
|
|
117
|
+
process = subprocess.Popen(
|
|
118
|
+
cmd,
|
|
119
|
+
stdout=open(log_file, "w"),
|
|
120
|
+
stderr=subprocess.STDOUT,
|
|
121
|
+
start_new_session=True # Run in its own session so it doesn't die with the server
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
self.update_task(task_id, status="running", pid=process.pid, started_at=datetime.isoformat(datetime.now()))
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.update_task(task_id, status="failed", error=str(e))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Tool interface functions
|
|
130
|
+
|
|
131
|
+
async def task_spawn(prompt: str, model: str = "gemini-3-flash") -> str:
|
|
132
|
+
"""Spawns a new background task."""
|
|
133
|
+
manager = BackgroundManager()
|
|
134
|
+
task_id = manager.create_task(prompt, model)
|
|
135
|
+
manager.spawn(task_id)
|
|
136
|
+
return f"Task spawned with ID: {task_id}. Use task_status('{task_id}') to check progress."
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def task_status(task_id: str) -> str:
|
|
140
|
+
"""Checks the status of a background task."""
|
|
141
|
+
manager = BackgroundManager()
|
|
142
|
+
task = manager.get_task(task_id)
|
|
143
|
+
if not task:
|
|
144
|
+
return f"Task {task_id} not found."
|
|
145
|
+
|
|
146
|
+
status = task["status"]
|
|
147
|
+
if status == "completed":
|
|
148
|
+
return f"Task {task_id} COMPLETED:\n\n{task.get('result')}"
|
|
149
|
+
elif status == "failed":
|
|
150
|
+
return f"Task {task_id} FAILED:\n\n{task.get('error')}"
|
|
151
|
+
else:
|
|
152
|
+
return f"Task {task_id} is currently {status} (PID: {task.get('pid')})."
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def task_list() -> str:
|
|
156
|
+
"""Lists all background tasks."""
|
|
157
|
+
manager = BackgroundManager()
|
|
158
|
+
tasks = manager.list_tasks()
|
|
159
|
+
if not tasks:
|
|
160
|
+
return "No background tasks found."
|
|
161
|
+
|
|
162
|
+
lines = ["Background Tasks:"]
|
|
163
|
+
for t in tasks:
|
|
164
|
+
lines.append(f"- [{t['id']}] {t['status']}: {t['prompt'][:50]}...")
|
|
165
|
+
|
|
166
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LSP Tools - Language Server Protocol Operations
|
|
3
|
+
|
|
4
|
+
These tools provide LSP functionality for Claude Code via subprocess calls
|
|
5
|
+
to language servers. Claude Code has native LSP support, so these serve as
|
|
6
|
+
supplementary utilities for advanced operations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def lsp_diagnostics(file_path: str, severity: str = "all") -> str:
|
|
16
|
+
"""
|
|
17
|
+
Get diagnostics (errors, warnings) for a file using language server.
|
|
18
|
+
|
|
19
|
+
For TypeScript/JavaScript, uses `tsc` or `biome`.
|
|
20
|
+
For Python, uses `pyright` or `ruff`.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
file_path: Path to the file to analyze
|
|
24
|
+
severity: Filter by severity (error, warning, information, hint, all)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Formatted diagnostics output.
|
|
28
|
+
"""
|
|
29
|
+
path = Path(file_path)
|
|
30
|
+
if not path.exists():
|
|
31
|
+
return f"Error: File not found: {file_path}"
|
|
32
|
+
|
|
33
|
+
suffix = path.suffix.lower()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
if suffix in (".ts", ".tsx", ".js", ".jsx"):
|
|
37
|
+
# Use TypeScript compiler for diagnostics
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["npx", "tsc", "--noEmit", "--pretty", str(path)],
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
timeout=30,
|
|
43
|
+
)
|
|
44
|
+
output = result.stdout + result.stderr
|
|
45
|
+
if not output.strip():
|
|
46
|
+
return "No diagnostics found"
|
|
47
|
+
return output
|
|
48
|
+
|
|
49
|
+
elif suffix == ".py":
|
|
50
|
+
# Use ruff for Python diagnostics
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["ruff", "check", str(path), "--output-format=text"],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
timeout=30,
|
|
56
|
+
)
|
|
57
|
+
output = result.stdout + result.stderr
|
|
58
|
+
if not output.strip():
|
|
59
|
+
return "No diagnostics found"
|
|
60
|
+
return output
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
return f"No diagnostics available for file type: {suffix}"
|
|
64
|
+
|
|
65
|
+
except FileNotFoundError as e:
|
|
66
|
+
return f"Tool not found: {e.filename}. Install required tools."
|
|
67
|
+
except subprocess.TimeoutExpired:
|
|
68
|
+
return "Diagnostics timed out"
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return f"Error: {str(e)}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def ast_grep_search(pattern: str, directory: str = ".", language: str = "") -> str:
|
|
74
|
+
"""
|
|
75
|
+
Search codebase using ast-grep for structural patterns.
|
|
76
|
+
|
|
77
|
+
ast-grep uses AST-aware pattern matching, finding code by structure
|
|
78
|
+
rather than just text. More precise than regex for code search.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
pattern: ast-grep pattern to search for
|
|
82
|
+
directory: Directory to search in
|
|
83
|
+
language: Filter by language (typescript, python, rust, etc.)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Matched code locations and snippets.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
cmd = ["sg", "run", "-p", pattern, directory]
|
|
90
|
+
if language:
|
|
91
|
+
cmd.extend(["--lang", language])
|
|
92
|
+
cmd.append("--json")
|
|
93
|
+
|
|
94
|
+
result = subprocess.run(
|
|
95
|
+
cmd,
|
|
96
|
+
capture_output=True,
|
|
97
|
+
text=True,
|
|
98
|
+
timeout=60,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if result.returncode != 0 and not result.stdout:
|
|
102
|
+
return result.stderr or "No matches found"
|
|
103
|
+
|
|
104
|
+
# Parse and format JSON output
|
|
105
|
+
try:
|
|
106
|
+
matches = json.loads(result.stdout)
|
|
107
|
+
if not matches:
|
|
108
|
+
return "No matches found"
|
|
109
|
+
|
|
110
|
+
lines = []
|
|
111
|
+
for match in matches[:20]: # Limit to 20 results
|
|
112
|
+
file_path = match.get("file", "unknown")
|
|
113
|
+
start_line = match.get("range", {}).get("start", {}).get("line", 0)
|
|
114
|
+
text = match.get("text", "")
|
|
115
|
+
lines.append(f"{file_path}:{start_line}: {text[:100]}")
|
|
116
|
+
|
|
117
|
+
return "\n".join(lines)
|
|
118
|
+
except json.JSONDecodeError:
|
|
119
|
+
return result.stdout or "No matches found"
|
|
120
|
+
|
|
121
|
+
except FileNotFoundError:
|
|
122
|
+
return "ast-grep (sg) not found. Install with: npm install -g @ast-grep/cli"
|
|
123
|
+
except subprocess.TimeoutExpired:
|
|
124
|
+
return "Search timed out"
|
|
125
|
+
except Exception as e:
|
|
126
|
+
return f"Error: {str(e)}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def grep_search(pattern: str, directory: str = ".", file_pattern: str = "") -> str:
|
|
130
|
+
"""
|
|
131
|
+
Fast text search using ripgrep.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
pattern: Search pattern (supports regex)
|
|
135
|
+
directory: Directory to search in
|
|
136
|
+
file_pattern: Glob pattern to filter files (e.g., "*.py", "*.ts")
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Matched lines with file paths and line numbers.
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
cmd = ["rg", "--line-number", "--max-count=50", pattern, directory]
|
|
143
|
+
if file_pattern:
|
|
144
|
+
cmd.extend(["--glob", file_pattern])
|
|
145
|
+
|
|
146
|
+
result = subprocess.run(
|
|
147
|
+
cmd,
|
|
148
|
+
capture_output=True,
|
|
149
|
+
text=True,
|
|
150
|
+
timeout=30,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
output = result.stdout
|
|
154
|
+
if not output.strip():
|
|
155
|
+
return "No matches found"
|
|
156
|
+
|
|
157
|
+
# Limit output lines
|
|
158
|
+
lines = output.strip().split("\n")
|
|
159
|
+
if len(lines) > 50:
|
|
160
|
+
lines = lines[:50]
|
|
161
|
+
lines.append(f"... and more (showing first 50 matches)")
|
|
162
|
+
|
|
163
|
+
return "\n".join(lines)
|
|
164
|
+
|
|
165
|
+
except FileNotFoundError:
|
|
166
|
+
return "ripgrep (rg) not found. Install with: brew install ripgrep"
|
|
167
|
+
except subprocess.TimeoutExpired:
|
|
168
|
+
return "Search timed out"
|
|
169
|
+
except Exception as e:
|
|
170
|
+
return f"Error: {str(e)}"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def glob_files(pattern: str, directory: str = ".") -> str:
|
|
174
|
+
"""
|
|
175
|
+
Find files matching a glob pattern.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts")
|
|
179
|
+
directory: Base directory for search
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of matching file paths.
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
cmd = ["fd", "--type", "f", "--glob", pattern, directory]
|
|
186
|
+
|
|
187
|
+
result = subprocess.run(
|
|
188
|
+
cmd,
|
|
189
|
+
capture_output=True,
|
|
190
|
+
text=True,
|
|
191
|
+
timeout=30,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
output = result.stdout
|
|
195
|
+
if not output.strip():
|
|
196
|
+
return "No files found"
|
|
197
|
+
|
|
198
|
+
# Limit output
|
|
199
|
+
lines = output.strip().split("\n")
|
|
200
|
+
if len(lines) > 100:
|
|
201
|
+
lines = lines[:100]
|
|
202
|
+
lines.append(f"... and {len(lines) - 100} more files")
|
|
203
|
+
|
|
204
|
+
return "\n".join(lines)
|
|
205
|
+
|
|
206
|
+
except FileNotFoundError:
|
|
207
|
+
return "fd not found. Install with: brew install fd"
|
|
208
|
+
except subprocess.TimeoutExpired:
|
|
209
|
+
return "Search timed out"
|
|
210
|
+
except Exception as e:
|
|
211
|
+
return f"Error: {str(e)}"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def ast_grep_replace(
|
|
215
|
+
pattern: str,
|
|
216
|
+
replacement: str,
|
|
217
|
+
directory: str = ".",
|
|
218
|
+
language: str = "",
|
|
219
|
+
dry_run: bool = True
|
|
220
|
+
) -> str:
|
|
221
|
+
"""
|
|
222
|
+
Replace code patterns using ast-grep's AST-aware replacement.
|
|
223
|
+
|
|
224
|
+
ast-grep uses structural pattern matching for precise code transformations.
|
|
225
|
+
More reliable than text-based search/replace for refactoring.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
pattern: ast-grep pattern to search for (e.g., "console.log($A)")
|
|
229
|
+
replacement: Replacement pattern (e.g., "logger.debug($A)")
|
|
230
|
+
directory: Directory to search in
|
|
231
|
+
language: Filter by language (typescript, python, rust, etc.)
|
|
232
|
+
dry_run: If True (default), only show what would change without applying
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Preview of changes or confirmation of applied changes.
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
# Build command
|
|
239
|
+
cmd = ["sg", "run", "-p", pattern, "-r", replacement, directory]
|
|
240
|
+
if language:
|
|
241
|
+
cmd.extend(["--lang", language])
|
|
242
|
+
|
|
243
|
+
if dry_run:
|
|
244
|
+
# Show what would change
|
|
245
|
+
cmd.append("--json")
|
|
246
|
+
result = subprocess.run(
|
|
247
|
+
cmd,
|
|
248
|
+
capture_output=True,
|
|
249
|
+
text=True,
|
|
250
|
+
timeout=60,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if result.returncode != 0 and not result.stdout:
|
|
254
|
+
return result.stderr or "No matches found"
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
matches = json.loads(result.stdout)
|
|
258
|
+
if not matches:
|
|
259
|
+
return "No matches found for pattern"
|
|
260
|
+
|
|
261
|
+
lines = [f"**Dry run** - {len(matches)} matches found:"]
|
|
262
|
+
for match in matches[:15]:
|
|
263
|
+
file_path = match.get("file", "unknown")
|
|
264
|
+
start_line = match.get("range", {}).get("start", {}).get("line", 0)
|
|
265
|
+
original = match.get("text", "")[:80]
|
|
266
|
+
lines.append(f"\n`{file_path}:{start_line}`")
|
|
267
|
+
lines.append(f"```\n{original}\n```")
|
|
268
|
+
|
|
269
|
+
if len(matches) > 15:
|
|
270
|
+
lines.append(f"\n... and {len(matches) - 15} more matches")
|
|
271
|
+
|
|
272
|
+
lines.append("\n**To apply changes**, call with `dry_run=False`")
|
|
273
|
+
return "\n".join(lines)
|
|
274
|
+
|
|
275
|
+
except json.JSONDecodeError:
|
|
276
|
+
return result.stdout or "No matches found"
|
|
277
|
+
else:
|
|
278
|
+
# Actually apply the changes
|
|
279
|
+
cmd_apply = ["sg", "run", "-p", pattern, "-r", replacement, directory, "--update-all"]
|
|
280
|
+
if language:
|
|
281
|
+
cmd_apply.extend(["--lang", language])
|
|
282
|
+
|
|
283
|
+
result = subprocess.run(
|
|
284
|
+
cmd_apply,
|
|
285
|
+
capture_output=True,
|
|
286
|
+
text=True,
|
|
287
|
+
timeout=60,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if result.returncode == 0:
|
|
291
|
+
return f"✅ Successfully applied replacement:\n- Pattern: `{pattern}`\n- Replacement: `{replacement}`\n\n{result.stdout}"
|
|
292
|
+
else:
|
|
293
|
+
return f"❌ Failed to apply replacement:\n{result.stderr}"
|
|
294
|
+
|
|
295
|
+
except FileNotFoundError:
|
|
296
|
+
return "ast-grep (sg) not found. Install with: npm install -g @ast-grep/cli"
|
|
297
|
+
except subprocess.TimeoutExpired:
|
|
298
|
+
return "Replacement timed out"
|
|
299
|
+
except Exception as e:
|
|
300
|
+
return f"Error: {str(e)}"
|
|
301
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Continuous Loop (Ralph Loop) for Stravinsky.
|
|
3
|
+
|
|
4
|
+
Allows Stravinsky to operate in an autonomous loop until criteria are met.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
async def enable_ralph_loop(goal: str, max_iterations: int = 10) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Enable continuous processing until a goal is met.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
goal: The goal to achieve and verify.
|
|
20
|
+
max_iterations: Maximum number of iterations before stopping.
|
|
21
|
+
"""
|
|
22
|
+
project_root = Path.cwd()
|
|
23
|
+
settings_file = project_root / ".claude" / "settings.json"
|
|
24
|
+
|
|
25
|
+
settings = {}
|
|
26
|
+
if settings_file.exists():
|
|
27
|
+
try:
|
|
28
|
+
settings = json.loads(settings_file.read_text())
|
|
29
|
+
except:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
if "hooks" not in settings:
|
|
33
|
+
settings["hooks"] = {}
|
|
34
|
+
|
|
35
|
+
# Set the Stop hook to re-trigger if goal not met
|
|
36
|
+
# Note: Stravinsky's prompt will handle the internal logic
|
|
37
|
+
# but the presence of this hook signals "Continue" to Claude Code.
|
|
38
|
+
settings["hooks"]["Stop"] = [
|
|
39
|
+
{
|
|
40
|
+
"type": "command",
|
|
41
|
+
"command": f'echo "Looping for goal: {goal}"',
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
settings_file.write_text(json.dumps(settings, indent=2))
|
|
47
|
+
|
|
48
|
+
return f"🔄 Ralph Loop ENABLED. Goal: {goal}. Stravinsky will now process continuously until completion."
|
|
49
|
+
|
|
50
|
+
async def disable_ralph_loop() -> str:
|
|
51
|
+
"""Disable the autonomous loop."""
|
|
52
|
+
project_root = Path.cwd()
|
|
53
|
+
settings_file = project_root / ".claude" / "settings.json"
|
|
54
|
+
|
|
55
|
+
if not settings_file.exists():
|
|
56
|
+
return "Ralph Loop is already disabled."
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
settings = json.loads(settings_file.read_text())
|
|
60
|
+
if "hooks" in settings and "Stop" in settings["hooks"]:
|
|
61
|
+
del settings["hooks"]["Stop"]
|
|
62
|
+
settings_file.write_text(json.dumps(settings, indent=2))
|
|
63
|
+
return "✅ Ralph Loop DISABLED."
|
|
64
|
+
except:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return "Failed to disable Ralph Loop or it was not active."
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LSP Tools Package
|
|
3
|
+
|
|
4
|
+
Provides Language Server Protocol functionality for code intelligence.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .tools import (
|
|
8
|
+
lsp_hover,
|
|
9
|
+
lsp_goto_definition,
|
|
10
|
+
lsp_find_references,
|
|
11
|
+
lsp_document_symbols,
|
|
12
|
+
lsp_workspace_symbols,
|
|
13
|
+
lsp_prepare_rename,
|
|
14
|
+
lsp_rename,
|
|
15
|
+
lsp_code_actions,
|
|
16
|
+
lsp_servers,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"lsp_hover",
|
|
21
|
+
"lsp_goto_definition",
|
|
22
|
+
"lsp_find_references",
|
|
23
|
+
"lsp_document_symbols",
|
|
24
|
+
"lsp_workspace_symbols",
|
|
25
|
+
"lsp_prepare_rename",
|
|
26
|
+
"lsp_rename",
|
|
27
|
+
"lsp_code_actions",
|
|
28
|
+
"lsp_servers",
|
|
29
|
+
]
|