stravinsky 0.1.2__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.

@@ -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,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
+ ]