scry-run 0.1.0__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.
- scry_run/__init__.py +102 -0
- scry_run/backends/__init__.py +6 -0
- scry_run/backends/base.py +65 -0
- scry_run/backends/claude.py +404 -0
- scry_run/backends/frozen.py +85 -0
- scry_run/backends/registry.py +72 -0
- scry_run/cache.py +441 -0
- scry_run/cli/__init__.py +137 -0
- scry_run/cli/apps.py +396 -0
- scry_run/cli/cache.py +342 -0
- scry_run/cli/config_cmd.py +84 -0
- scry_run/cli/env.py +27 -0
- scry_run/cli/init.py +375 -0
- scry_run/cli/run.py +71 -0
- scry_run/config.py +141 -0
- scry_run/console.py +52 -0
- scry_run/context.py +298 -0
- scry_run/generator.py +698 -0
- scry_run/home.py +60 -0
- scry_run/logging.py +171 -0
- scry_run/meta.py +1852 -0
- scry_run/packages.py +175 -0
- scry_run-0.1.0.dist-info/METADATA +282 -0
- scry_run-0.1.0.dist-info/RECORD +26 -0
- scry_run-0.1.0.dist-info/WHEEL +4 -0
- scry_run-0.1.0.dist-info/entry_points.txt +2 -0
scry_run/cli/cache.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Cache management commands."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from scry_run.cache import ScryCache
|
|
11
|
+
from scry_run.home import get_app_dir, get_apps_dir
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def cache() -> None:
|
|
18
|
+
"""Manage the scry-run code cache."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cache.command("list")
|
|
23
|
+
@click.argument("app_name", required=False)
|
|
24
|
+
@click.option("--keys", "-k", is_flag=True, help="Output keys only (Class.Attribute)")
|
|
25
|
+
def list_entries(app_name: Optional[str], keys: bool) -> None:
|
|
26
|
+
"""List cached code entries.
|
|
27
|
+
|
|
28
|
+
Without app name, lists all apps and their entry counts.
|
|
29
|
+
With app name, lists entries for that specific app.
|
|
30
|
+
"""
|
|
31
|
+
if app_name:
|
|
32
|
+
# List entries for specific app
|
|
33
|
+
app_dir = get_app_dir(app_name)
|
|
34
|
+
cache_file = app_dir / "cache.json"
|
|
35
|
+
|
|
36
|
+
if not cache_file.exists():
|
|
37
|
+
console.print(f"[red]App '{app_name}' not found or has no cache.[/red]")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
llm_cache = ScryCache(cache_file)
|
|
41
|
+
entries = llm_cache.list_entries()
|
|
42
|
+
|
|
43
|
+
if not entries:
|
|
44
|
+
if not keys:
|
|
45
|
+
console.print(f"[yellow]No cached entries for app '{app_name}'.[/yellow]")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if keys:
|
|
49
|
+
for entry in sorted(entries, key=lambda e: (e.class_name, e.attr_name)):
|
|
50
|
+
console.print(f"{entry.class_name}.{entry.attr_name}")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
table = Table(title=f"Cached Code Entries ({app_name})")
|
|
54
|
+
table.add_column("Class", style="cyan")
|
|
55
|
+
table.add_column("Attribute", style="green")
|
|
56
|
+
table.add_column("Type", style="magenta")
|
|
57
|
+
table.add_column("Created", style="dim")
|
|
58
|
+
table.add_column("Description")
|
|
59
|
+
|
|
60
|
+
for entry in sorted(entries, key=lambda e: (e.class_name, e.attr_name)):
|
|
61
|
+
# Truncate long descriptions
|
|
62
|
+
desc = entry.docstring[:50] + "..." if len(entry.docstring) > 50 else entry.docstring
|
|
63
|
+
|
|
64
|
+
table.add_row(
|
|
65
|
+
entry.class_name,
|
|
66
|
+
entry.attr_name,
|
|
67
|
+
entry.code_type,
|
|
68
|
+
entry.created_at[:10], # Just the date
|
|
69
|
+
desc,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
console.print(table)
|
|
73
|
+
console.print(f"\n[dim]Total: {len(entries)} entries[/dim]")
|
|
74
|
+
else:
|
|
75
|
+
# List all apps with entry counts
|
|
76
|
+
apps_dir = get_apps_dir()
|
|
77
|
+
if not apps_dir.exists():
|
|
78
|
+
console.print("[yellow]No apps found.[/yellow]")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
app_paths = [p for p in apps_dir.iterdir() if p.is_dir()]
|
|
82
|
+
if not app_paths:
|
|
83
|
+
console.print("[yellow]No apps found.[/yellow]")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
for app_path in sorted(app_paths):
|
|
87
|
+
cache_file = app_path / "cache.json"
|
|
88
|
+
if cache_file.exists():
|
|
89
|
+
llm_cache = ScryCache(cache_file)
|
|
90
|
+
entries = llm_cache.list_entries()
|
|
91
|
+
console.print(f"{app_path.name}: {len(entries)} entries")
|
|
92
|
+
else:
|
|
93
|
+
console.print(f"{app_path.name}: 0 entries")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@cache.command("show")
|
|
97
|
+
@click.argument("app_name")
|
|
98
|
+
@click.argument("key")
|
|
99
|
+
def show_entry(app_name: str, key: str) -> None:
|
|
100
|
+
"""Show details of a specific cached entry.
|
|
101
|
+
|
|
102
|
+
KEY should be in format Class.method (e.g., MyApp.process_data).
|
|
103
|
+
"""
|
|
104
|
+
app_dir = get_app_dir(app_name)
|
|
105
|
+
cache_file = app_dir / "cache.json"
|
|
106
|
+
|
|
107
|
+
if not cache_file.exists():
|
|
108
|
+
console.print(f"[red]App '{app_name}' not found or has no cache.[/red]")
|
|
109
|
+
raise SystemExit(1)
|
|
110
|
+
|
|
111
|
+
# Parse key into class_name and attr_name
|
|
112
|
+
if "." not in key:
|
|
113
|
+
console.print(f"[red]Invalid key format:[/red] {key}")
|
|
114
|
+
console.print("[dim]Expected format: Class.method (e.g., MyApp.process_data)[/dim]")
|
|
115
|
+
raise SystemExit(1)
|
|
116
|
+
|
|
117
|
+
class_name, attr_name = key.split(".", 1)
|
|
118
|
+
|
|
119
|
+
llm_cache = ScryCache(cache_file)
|
|
120
|
+
entry = llm_cache.get(class_name, attr_name)
|
|
121
|
+
|
|
122
|
+
if not entry:
|
|
123
|
+
console.print(f"[red]Entry not found:[/red] {key} in app '{app_name}'")
|
|
124
|
+
raise SystemExit(1)
|
|
125
|
+
|
|
126
|
+
console.print(f"[bold cyan]{entry.class_name}[/bold cyan].[bold green]{entry.attr_name}[/bold green]")
|
|
127
|
+
console.print(f"[dim]Type:[/dim] {entry.code_type}")
|
|
128
|
+
console.print(f"[dim]Created:[/dim] {entry.created_at}")
|
|
129
|
+
console.print(f"[dim]Checksum:[/dim] {entry.checksum[:16]}...")
|
|
130
|
+
console.print()
|
|
131
|
+
|
|
132
|
+
if entry.docstring:
|
|
133
|
+
console.print(f"[bold]Description:[/bold] {entry.docstring}")
|
|
134
|
+
console.print()
|
|
135
|
+
|
|
136
|
+
if entry.dependencies:
|
|
137
|
+
console.print("[bold]Dependencies:[/bold]")
|
|
138
|
+
for dep in entry.dependencies:
|
|
139
|
+
console.print(f" {dep}")
|
|
140
|
+
console.print()
|
|
141
|
+
|
|
142
|
+
console.print("[bold]Code:[/bold]")
|
|
143
|
+
console.print("```python")
|
|
144
|
+
console.print(entry.code)
|
|
145
|
+
console.print("```")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@cache.command("export")
|
|
149
|
+
@click.argument("app_name")
|
|
150
|
+
@click.option(
|
|
151
|
+
"--output", "-o",
|
|
152
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
153
|
+
help="Output file path (default: stdout as JSON)",
|
|
154
|
+
)
|
|
155
|
+
@click.option(
|
|
156
|
+
"--format", "-f",
|
|
157
|
+
type=click.Choice(["json", "python"]),
|
|
158
|
+
default="json",
|
|
159
|
+
help="Output format",
|
|
160
|
+
)
|
|
161
|
+
def export_cache(
|
|
162
|
+
app_name: str,
|
|
163
|
+
output: Optional[Path],
|
|
164
|
+
format: str,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Export cached code for an app."""
|
|
167
|
+
import json
|
|
168
|
+
|
|
169
|
+
app_dir = get_app_dir(app_name)
|
|
170
|
+
cache_file = app_dir / "cache.json"
|
|
171
|
+
|
|
172
|
+
if not cache_file.exists():
|
|
173
|
+
console.print(f"[red]App '{app_name}' not found or has no cache.[/red]")
|
|
174
|
+
raise SystemExit(1)
|
|
175
|
+
|
|
176
|
+
llm_cache = ScryCache(cache_file)
|
|
177
|
+
|
|
178
|
+
if format == "python":
|
|
179
|
+
if not output:
|
|
180
|
+
console.print("[red]Error:[/red] --output required for Python format")
|
|
181
|
+
raise SystemExit(1)
|
|
182
|
+
|
|
183
|
+
llm_cache.export_to_file(output)
|
|
184
|
+
console.print(f"[green]Exported to:[/green] {output}")
|
|
185
|
+
else:
|
|
186
|
+
data = llm_cache.export()
|
|
187
|
+
|
|
188
|
+
if output:
|
|
189
|
+
with open(output, "w") as f:
|
|
190
|
+
json.dump(data, f, indent=2)
|
|
191
|
+
console.print(f"[green]Exported to:[/green] {output}")
|
|
192
|
+
else:
|
|
193
|
+
console.print_json(data=data)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@cache.command("prune")
|
|
197
|
+
@click.argument("app_name")
|
|
198
|
+
@click.option(
|
|
199
|
+
"--class", "class_name",
|
|
200
|
+
help="Only prune entries for this class",
|
|
201
|
+
)
|
|
202
|
+
@click.option(
|
|
203
|
+
"--attr", "attr_name",
|
|
204
|
+
help="Only prune this specific attribute (requires --class)",
|
|
205
|
+
)
|
|
206
|
+
@click.option(
|
|
207
|
+
"--all", "prune_all",
|
|
208
|
+
is_flag=True,
|
|
209
|
+
help="Prune all entries without confirmation",
|
|
210
|
+
)
|
|
211
|
+
def prune_cache(
|
|
212
|
+
app_name: str,
|
|
213
|
+
class_name: Optional[str],
|
|
214
|
+
attr_name: Optional[str],
|
|
215
|
+
prune_all: bool,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Remove cached entries for an app."""
|
|
218
|
+
from rich.prompt import Confirm
|
|
219
|
+
|
|
220
|
+
app_dir = get_app_dir(app_name)
|
|
221
|
+
cache_file = app_dir / "cache.json"
|
|
222
|
+
|
|
223
|
+
if not cache_file.exists():
|
|
224
|
+
console.print(f"[red]App '{app_name}' not found or has no cache.[/red]")
|
|
225
|
+
raise SystemExit(1)
|
|
226
|
+
|
|
227
|
+
llm_cache = ScryCache(cache_file)
|
|
228
|
+
|
|
229
|
+
if attr_name and not class_name:
|
|
230
|
+
console.print("[red]Error:[/red] --attr requires --class")
|
|
231
|
+
raise SystemExit(1)
|
|
232
|
+
|
|
233
|
+
# Describe what will be pruned
|
|
234
|
+
if class_name and attr_name:
|
|
235
|
+
target = f"{class_name}.{attr_name}"
|
|
236
|
+
elif class_name:
|
|
237
|
+
target = f"all entries for {class_name}"
|
|
238
|
+
else:
|
|
239
|
+
target = "all cached entries"
|
|
240
|
+
|
|
241
|
+
# Confirm unless --all flag
|
|
242
|
+
if not prune_all:
|
|
243
|
+
entries = llm_cache.list_entries()
|
|
244
|
+
if class_name:
|
|
245
|
+
entries = [e for e in entries if e.class_name == class_name]
|
|
246
|
+
if attr_name:
|
|
247
|
+
entries = [e for e in entries if e.attr_name == attr_name]
|
|
248
|
+
|
|
249
|
+
if not entries:
|
|
250
|
+
console.print(f"[yellow]No matching entries found for app '{app_name}'.[/yellow]")
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
console.print(f"Will prune [bold]{len(entries)}[/bold] entries: {target}")
|
|
254
|
+
|
|
255
|
+
if not Confirm.ask("Continue?"):
|
|
256
|
+
console.print("[yellow]Aborted.[/yellow]")
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
# Prune
|
|
260
|
+
count = llm_cache.prune(class_name=class_name, attr_name=attr_name)
|
|
261
|
+
|
|
262
|
+
if count > 0:
|
|
263
|
+
console.print(f"[green]Pruned {count} entries.[/green]")
|
|
264
|
+
else:
|
|
265
|
+
console.print("[yellow]No entries to prune.[/yellow]")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@cache.command("rm")
|
|
269
|
+
@click.argument("app_name")
|
|
270
|
+
@click.argument("keys", nargs=-1)
|
|
271
|
+
def remove_entry(app_name: str, keys: tuple[str, ...]) -> None:
|
|
272
|
+
"""Remove specific cache entries from an app.
|
|
273
|
+
|
|
274
|
+
KEY format:
|
|
275
|
+
- ClassName.method_name (remove specific method)
|
|
276
|
+
- ClassName (remove all entries for class)
|
|
277
|
+
"""
|
|
278
|
+
if not keys:
|
|
279
|
+
console.print("[yellow]No keys provided. usage: scry-run cache rm <app-name> Class.method[/yellow]")
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
app_dir = get_app_dir(app_name)
|
|
283
|
+
cache_file = app_dir / "cache.json"
|
|
284
|
+
|
|
285
|
+
if not cache_file.exists():
|
|
286
|
+
console.print(f"[red]App '{app_name}' not found or has no cache.[/red]")
|
|
287
|
+
raise SystemExit(1)
|
|
288
|
+
|
|
289
|
+
llm_cache = ScryCache(cache_file)
|
|
290
|
+
total_pruned = 0
|
|
291
|
+
|
|
292
|
+
for key in keys:
|
|
293
|
+
if "." in key:
|
|
294
|
+
# Remove specific attribute
|
|
295
|
+
class_name, attr_name = key.split(".", 1)
|
|
296
|
+
if llm_cache.delete(class_name, attr_name):
|
|
297
|
+
console.print(f"[green]Removed[/green] {class_name}.{attr_name}")
|
|
298
|
+
total_pruned += 1
|
|
299
|
+
else:
|
|
300
|
+
console.print(f"[red]Not found:[/red] {class_name}.{attr_name}")
|
|
301
|
+
else:
|
|
302
|
+
# Remove entire class
|
|
303
|
+
class_name = key
|
|
304
|
+
pruned = llm_cache.prune(class_name=class_name)
|
|
305
|
+
if pruned > 0:
|
|
306
|
+
console.print(f"[green]Removed[/green] {class_name} ({pruned} entries)")
|
|
307
|
+
total_pruned += pruned
|
|
308
|
+
else:
|
|
309
|
+
console.print(f"[red]Not found:[/red] {class_name}")
|
|
310
|
+
|
|
311
|
+
if total_pruned > 0:
|
|
312
|
+
console.print(f"\n[bold green]Total removed: {total_pruned}[/bold green]")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@cache.command("reset")
|
|
316
|
+
@click.argument("app_name")
|
|
317
|
+
@click.option(
|
|
318
|
+
"--force", "-f",
|
|
319
|
+
is_flag=True,
|
|
320
|
+
help="Force reset without confirmation",
|
|
321
|
+
)
|
|
322
|
+
def reset_cache(app_name: str, force: bool) -> None:
|
|
323
|
+
"""Reset the entire cache for an app (delete all entries)."""
|
|
324
|
+
from rich.prompt import Confirm
|
|
325
|
+
|
|
326
|
+
app_dir = get_app_dir(app_name)
|
|
327
|
+
cache_file = app_dir / "cache.json"
|
|
328
|
+
|
|
329
|
+
if not cache_file.exists():
|
|
330
|
+
console.print(f"[red]App '{app_name}' not found or has no cache.[/red]")
|
|
331
|
+
raise SystemExit(1)
|
|
332
|
+
|
|
333
|
+
if not force:
|
|
334
|
+
console.print(f"[bold red]WARNING:[/bold red] This will delete ALL cached code for app '{app_name}'.")
|
|
335
|
+
if not Confirm.ask("Are you sure you want to reset the cache?"):
|
|
336
|
+
console.print("[yellow]Aborted.[/yellow]")
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
llm_cache = ScryCache(cache_file)
|
|
340
|
+
count = llm_cache.prune()
|
|
341
|
+
|
|
342
|
+
console.print(f"[bold green]Cache reset complete. Removed {count} entries.[/bold green]")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Config command for scry-run."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shlex
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from scry_run.home import get_config_path
|
|
13
|
+
from scry_run.config import DEFAULT_CONFIG
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_editor() -> list[str] | None:
|
|
19
|
+
"""Get the editor command to use, with sensible fallbacks.
|
|
20
|
+
|
|
21
|
+
Priority:
|
|
22
|
+
1. VISUAL env var
|
|
23
|
+
2. EDITOR env var
|
|
24
|
+
3. First available: nano, vim, vi, notepad (Windows)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of command parts (e.g., ["code", "--wait"]) or None
|
|
28
|
+
"""
|
|
29
|
+
# Check env vars first (may contain args like "code --wait")
|
|
30
|
+
if editor := os.environ.get("VISUAL"):
|
|
31
|
+
return shlex.split(editor)
|
|
32
|
+
if editor := os.environ.get("EDITOR"):
|
|
33
|
+
return shlex.split(editor)
|
|
34
|
+
|
|
35
|
+
# Sensible fallbacks
|
|
36
|
+
fallbacks = ["nano", "vim", "vi"]
|
|
37
|
+
if sys.platform == "win32":
|
|
38
|
+
fallbacks = ["notepad", "code", "vim"]
|
|
39
|
+
|
|
40
|
+
for editor in fallbacks:
|
|
41
|
+
if shutil.which(editor):
|
|
42
|
+
return [editor]
|
|
43
|
+
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@click.command("config")
|
|
48
|
+
def config_cmd() -> None:
|
|
49
|
+
"""Open the scry-run config file in your editor.
|
|
50
|
+
|
|
51
|
+
Creates a default config file if it doesn't exist.
|
|
52
|
+
|
|
53
|
+
Editor selection (in order):
|
|
54
|
+
1. $VISUAL
|
|
55
|
+
2. $EDITOR
|
|
56
|
+
3. nano, vim, vi (or notepad on Windows)
|
|
57
|
+
"""
|
|
58
|
+
config_path = get_config_path()
|
|
59
|
+
|
|
60
|
+
# Create config file with defaults if it doesn't exist
|
|
61
|
+
if not config_path.exists():
|
|
62
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
config_path.write_text(DEFAULT_CONFIG)
|
|
64
|
+
console.print(f"[green]Created config file:[/green] {config_path}")
|
|
65
|
+
|
|
66
|
+
# Get editor
|
|
67
|
+
editor_cmd = get_editor()
|
|
68
|
+
if not editor_cmd:
|
|
69
|
+
console.print("[red]Error:[/red] No editor found.")
|
|
70
|
+
console.print("Set VISUAL or EDITOR environment variable, or install nano/vim.")
|
|
71
|
+
raise SystemExit(1)
|
|
72
|
+
|
|
73
|
+
editor_display = " ".join(editor_cmd)
|
|
74
|
+
console.print(f"[dim]Opening {config_path} with {editor_display}...[/dim]")
|
|
75
|
+
|
|
76
|
+
# Open editor (append file path to command)
|
|
77
|
+
try:
|
|
78
|
+
subprocess.run(editor_cmd + [str(config_path)], check=True)
|
|
79
|
+
except subprocess.CalledProcessError as e:
|
|
80
|
+
console.print(f"[red]Error:[/red] Editor exited with code {e.returncode}")
|
|
81
|
+
raise SystemExit(e.returncode)
|
|
82
|
+
except FileNotFoundError:
|
|
83
|
+
console.print(f"[red]Error:[/red] Editor '{editor_cmd[0]}' not found")
|
|
84
|
+
raise SystemExit(1)
|
scry_run/cli/env.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Env command for displaying configuration as environment variables."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from scry_run.config import load_config, get_env_vars
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command()
|
|
9
|
+
def env() -> None:
|
|
10
|
+
"""Display configuration as shell environment variables.
|
|
11
|
+
|
|
12
|
+
Outputs export statements that can be eval'd in your shell:
|
|
13
|
+
|
|
14
|
+
eval "$(scry-run env)"
|
|
15
|
+
|
|
16
|
+
This allows running apps directly without scry-run run:
|
|
17
|
+
|
|
18
|
+
eval "$(scry-run env)"
|
|
19
|
+
~/.scry-run/apps/todo-app/app.py add "Buy milk"
|
|
20
|
+
"""
|
|
21
|
+
config = load_config()
|
|
22
|
+
env_vars = get_env_vars(config)
|
|
23
|
+
|
|
24
|
+
# Output as shell export statements
|
|
25
|
+
for key, value in sorted(env_vars.items()):
|
|
26
|
+
# Quote values to handle special characters
|
|
27
|
+
click.echo(f'export {key}="{value}"')
|